From 89c5a62612786095cd9a201765d0bd049cb3b4fd Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 16:47:13 +0700 Subject: [PATCH 01/16] feat: update updated date of memo when if there are changes --- Memola/App/MemolaApp.swift | 3 +++ Memola/Canvas/History/History.swift | 18 ++++++++++++++ Memola/Canvas/Tool/Core/Tool.swift | 10 +++++--- Memola/Canvas/Tool/Pen/Core/Pen.swift | 4 ++++ .../ViewController/CanvasViewController.swift | 2 -- Memola/Features/Memo/Memo/MemoView.swift | 3 ++- .../Memo/PenDock/PenDropDelegate.swift | 3 ++- Memola/Features/Memo/Toolbar/Toolbar.swift | 6 ++++- Memola/Features/Memos/MemosView.swift | 24 +++++++++++++------ 9 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Memola/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index 4804ebc..7ee7826 100644 --- a/Memola/App/MemolaApp.swift +++ b/Memola/App/MemolaApp.swift @@ -14,6 +14,9 @@ struct MemolaApp: App { MemosView() .persistence(\.viewContext) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } withPersistenceSync(\.backgroundContext) { context in try context.saveIfNeeded() } diff --git a/Memola/Canvas/History/History.swift b/Memola/Canvas/History/History.swift index a6d152b..a23b8eb 100644 --- a/Memola/Canvas/History/History.swift +++ b/Memola/Canvas/History/History.swift @@ -9,6 +9,12 @@ import Combine import Foundation class History: ObservableObject { + var memo: MemoObject? + + init(memo: MemoObject?) { + self.memo = memo + } + @Published var undoStack: [HistoryEvent] = [] @Published var redoStack: [HistoryEvent] = [] @@ -41,10 +47,18 @@ class History: ObservableObject { func addUndo(_ event: HistoryEvent) { undoStack.append(event) + withPersistence(\.viewContext) { [weak memo] context in + memo?.updatedAt = .now + try context.saveIfNeeded() + } } func addRedo(_ event: HistoryEvent) { redoStack.append(event) + withPersistence(\.viewContext) { [weak memo] context in + memo?.updatedAt = .now + try context.saveIfNeeded() + } } func resetRedo() { @@ -87,6 +101,10 @@ class History: ObservableObject { } } redoStack.removeAll() + withPersistence(\.viewContext) { [weak memo] context in + memo?.updatedAt = .now + try context.saveIfNeeded() + } } func restoreUndo() { diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 18502f7..38d9b9e 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -38,6 +38,7 @@ public class Tool: NSObject, ObservableObject { self.selection = selection withPersistence(\.viewContext) { [weak object] context in object?.selection = selection.rawValue + object?.memo?.updatedAt = .now try context.saveIfNeeded() } } @@ -85,10 +86,11 @@ public class Tool: NSObject, ObservableObject { pens.insert(pen, at: index + 1) } selectPen(pen) - withPersistence(\.viewContext) { [pens] context in + withPersistence(\.viewContext) { [pens, weak object] context in for (index, pen) in pens.enumerated() { pen.object?.orderIndex = Int16(index) } + object?.memo?.updatedAt = .now try context.saveIfNeeded() } } @@ -102,7 +104,8 @@ public class Tool: NSObject, ObservableObject { object.pens.add(_pen) } scrollPublisher.send(pen.id) - withPersistence(\.viewContext) { context in + withPersistence(\.viewContext) { [weak object] context in + object?.memo?.updatedAt = .now try context.saveIfNeeded() } } @@ -116,8 +119,9 @@ public class Tool: NSObject, ObservableObject { if let _pen = deletedPen.object { _pen.tool = nil object.pens.remove(_pen) - withPersistence(\.viewContext) { context in + withPersistence(\.viewContext) { [weak object] context in context.delete(_pen) + object?.memo?.updatedAt = .now try context.saveIfNeeded() } } diff --git a/Memola/Canvas/Tool/Pen/Core/Pen.swift b/Memola/Canvas/Tool/Pen/Core/Pen.swift index 81f461a..9195a6c 100644 --- a/Memola/Canvas/Tool/Pen/Core/Pen.swift +++ b/Memola/Canvas/Tool/Pen/Core/Pen.swift @@ -16,21 +16,25 @@ class Pen: NSObject, ObservableObject, Identifiable { @Published var style: any PenStyle { didSet { object?.style = strokeStyle.rawValue + object?.tool?.memo?.updatedAt = .now } } @Published var rgba: [CGFloat] { didSet { object?.color = rgba + object?.tool?.memo?.updatedAt = .now } } @Published var thickness: CGFloat { didSet { object?.thickness = thickness + object?.tool?.memo?.updatedAt = .now } } @Published var isSelected: Bool { didSet { object?.isSelected = isSelected + object?.tool?.memo?.updatedAt = .now } } var color: Color { diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 395b096..6ec4d48 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -259,7 +259,6 @@ extension CanvasViewController: UIScrollViewDelegate { func scrollViewDidZoom(_ scrollView: UIScrollView) { canvas.setZoomScale(scrollView.zoomScale) -// renderer.resize(on: renderView, to: renderView.drawableSize) renderView.draw() } @@ -274,7 +273,6 @@ extension CanvasViewController: UIScrollViewDelegate { } func scrollViewDidScroll(_ scrollView: UIScrollView) { -// renderer.resize(on: renderView, to: renderView.drawableSize) renderView.draw() } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index afc3b56..4a7a384 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -11,7 +11,7 @@ import CoreData struct MemoView: View { @StateObject var tool: Tool @StateObject var canvas: Canvas - @StateObject var history = History() + @StateObject var history: History @State var memo: MemoObject @State var title: String @@ -24,6 +24,7 @@ struct MemoView: View { self.title = memo.title self._tool = StateObject(wrappedValue: Tool(object: memo.tool)) self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID, gridMode: memo.canvas.gridMode)) + self._history = StateObject(wrappedValue: History(memo: memo)) } var body: some View { diff --git a/Memola/Features/Memo/PenDock/PenDropDelegate.swift b/Memola/Features/Memo/PenDock/PenDropDelegate.swift index 7d91630..8557f34 100644 --- a/Memola/Features/Memo/PenDock/PenDropDelegate.swift +++ b/Memola/Features/Memo/PenDock/PenDropDelegate.swift @@ -29,10 +29,11 @@ struct PenDropDelegate: DropDelegate { tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) tool.objectWillChange.send() } - withPersistence(\.viewContext) { context in + withPersistence(\.viewContext) { [weak object = tool.object] context in for (index, pen) in tool.pens.enumerated() { pen.object?.orderIndex = Int16(index) } + object?.memo?.updatedAt = .now try context.saveIfNeeded() } } diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 3139d53..bb99cac 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -123,6 +123,7 @@ struct Toolbar: View { if !newValue { if !title.isEmpty { memo.title = title + memo.updatedAt = .now } else { title = memo.title } @@ -313,8 +314,11 @@ struct Toolbar: View { withAnimation { canvas.state = .closing } + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } withPersistence(\.backgroundContext) { context in - try? context.saveIfNeeded() + try context.saveIfNeeded() context.refreshAllObjects() DispatchQueue.main.async { withAnimation { diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 680f278..bdf4e07 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -14,6 +14,9 @@ struct MemosView: View { @State var memo: MemoObject? + let cellWidth: CGFloat = 250 + let cellHeight: CGFloat = 150 + var body: some View { NavigationStack { memoGrid @@ -41,21 +44,28 @@ struct MemosView: View { } var memoGrid: some View { - ScrollView { - LazyVGrid(columns: .init(repeating: GridItem(.flexible()), count: 3)) { - ForEach(memoObjects) { memo in - memoCard(memo) + GeometryReader { proxy in + let count = Int(proxy.size.width / cellWidth) + let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) + ScrollView { + LazyVGrid(columns: columns, spacing: 15) { + ForEach(memoObjects) { memo in + memoCard(memo) + } } + .padding() } - .padding() } } func memoCard(_ memoObject: MemoObject) -> some View { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { Rectangle() - .frame(height: 150) + .frame(height: cellHeight) + .clipShape(RoundedRectangle(cornerRadius: 10)) Text(memoObject.title) + .font(.headline) + .fontWeight(.semibold) } .onTapGesture { openMemo(for: memoObject) From 9508f3cf136dcf7f0c7f59c677778bf66794498a Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 20:08:54 +0700 Subject: [PATCH 02/16] feat: add sort options --- Memola.xcodeproj/project.pbxproj | 4 +++ Memola/Features/Memos/MemosView.swift | 29 ++++++++++++++-- Memola/Features/Memos/Sort.swift | 49 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 Memola/Features/Memos/Sort.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index e57b134..131c62d 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; }; EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; }; + EC1815082C2D980B00541369 /* Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815072C2D980B00541369 /* Sort.swift */; }; EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; }; EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; }; @@ -117,6 +118,7 @@ EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = ""; }; + EC1815072C2D980B00541369 /* Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sort.swift; sourceTree = ""; }; EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = ""; }; EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; @@ -411,6 +413,7 @@ isa = PBXGroup; children = ( ECA738792BE5EF0400A4542E /* MemosView.swift */, + EC1815072C2D980B00541369 /* Sort.swift */, ); path = Memos; sourceTree = ""; @@ -887,6 +890,7 @@ EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */, ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, + EC1815082C2D980B00541369 /* Sort.swift in Sources */, ECA7387D2BE5EF4B00A4542E /* MemoView.swift in Sources */, ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */, diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index bdf4e07..1c292a1 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -10,23 +10,43 @@ import SwiftUI struct MemosView: View { @Environment(\.managedObjectContext) var managedObjectContext - @FetchRequest(sortDescriptors: []) var memoObjects: FetchedResults + @FetchRequest var memoObjects: FetchedResults @State var memo: MemoObject? + @AppStorage("memola.memo-objects.sort") var sort: Sort = .recent + let cellWidth: CGFloat = 250 let cellHeight: CGFloat = 150 + init() { + let standard = UserDefaults.standard + var descriptors: [SortDescriptor] = [] + let sort = Sort(rawValue: standard.value(forKey: "memola.memo-objects.sort") as? String ?? "") ?? .recent + descriptors = sort.memoSortDescriptors + _memoObjects = FetchRequest(sortDescriptors: descriptors) + } + var body: some View { NavigationStack { memoGrid .navigationTitle("Memos") .toolbar { - ToolbarItem(placement: .primaryAction) { + ToolbarItemGroup(placement: .primaryAction) { + Menu { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + } Button { createMemo(title: "Untitled") } label: { - Image(systemName: "plus") + Image(systemName: "square.and.pencil") } .hoverEffect() } @@ -41,6 +61,9 @@ struct MemosView: View { } } } + .onChange(of: sort) { oldValue, newValue in + memoObjects.sortDescriptors = newValue.memoSortDescriptors + } } var memoGrid: some View { diff --git a/Memola/Features/Memos/Sort.swift b/Memola/Features/Memos/Sort.swift new file mode 100644 index 0000000..aa9f4b0 --- /dev/null +++ b/Memola/Features/Memos/Sort.swift @@ -0,0 +1,49 @@ +// +// Sort.swift +// Memola +// +// Created by Dscyre Scotti on 6/27/24. +// + +import Foundation + +enum Sort: String, Identifiable, Hashable, Equatable { + var id: String { + rawValue + } + + case recent + case aToZ + case zToA + case newest + case oldest + + var name: String { + switch self { + case .recent: return "Recent" + case .aToZ: return "A to Z" + case .zToA: return "Z to A" + case .newest: return "Newest" + case .oldest: return "Oldest" + } + } + + static let all: [Sort] = [.recent, .aToZ, .zToA, .newest, .oldest] +} + +extension Sort { + var memoSortDescriptors: [SortDescriptor] { + switch self { + case .recent: + return [SortDescriptor(\.updatedAt, order: .reverse)] + case .aToZ: + return [SortDescriptor(\.title), SortDescriptor(\.updatedAt, order: .reverse)] + case .zToA: + return [SortDescriptor(\.title, order: .reverse), SortDescriptor(\.updatedAt, order: .reverse)] + case .newest: + return [SortDescriptor(\.createdAt, order: .reverse)] + case .oldest: + return [SortDescriptor(\.createdAt)] + } + } +} From b5b12f6540786451fe18280cd7741f727ece8c98 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 20:27:23 +0700 Subject: [PATCH 03/16] feat: add search bar --- Memola/Features/Memos/MemosView.swift | 47 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 1c292a1..9790539 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -13,6 +13,7 @@ struct MemosView: View { @FetchRequest var memoObjects: FetchedResults @State var memo: MemoObject? + @State var query: String = "" @AppStorage("memola.memo-objects.sort") var sort: Sort = .recent @@ -31,24 +32,28 @@ struct MemosView: View { NavigationStack { memoGrid .navigationTitle("Memos") + .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) .toolbar { ToolbarItemGroup(placement: .primaryAction) { - Menu { - Picker("", selection: $sort) { - ForEach(Sort.all) { sort in - Text(sort.name) - .tag(sort) - } + HStack(spacing: 5) { + Button { + createMemo(title: "Untitled") + } label: { + Image(systemName: "square.and.pencil") } - } label: { - Image(systemName: "arrow.up.arrow.down.circle") + .hoverEffect(.lift) + Menu { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + } + .hoverEffect(.lift) } - Button { - createMemo(title: "Untitled") - } label: { - Image(systemName: "square.and.pencil") - } - .hoverEffect() } } } @@ -64,6 +69,9 @@ struct MemosView: View { .onChange(of: sort) { oldValue, newValue in memoObjects.sortDescriptors = newValue.memoSortDescriptors } + .onChange(of: query) { oldValue, newValue in + updatePredicate() + } } var memoGrid: some View { @@ -148,4 +156,15 @@ struct MemosView: View { func openMemo(for memo: MemoObject) { self.memo = memo } + + func updatePredicate() { + var predicates: [NSPredicate] = [] + if !query.isEmpty { + predicates.append(NSPredicate(format: "title contains[c] %@", query)) + } +// if filter == .favorites { +// predicates.append(NSPredicate(format: "isFavorite = YES")) +// } + memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) + } } From f9c91794916f66c9dd00a35e5ec1b3013ffc3079 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 20:40:51 +0700 Subject: [PATCH 04/16] feat: add filter options --- Memola.xcodeproj/project.pbxproj | 4 +++ Memola/Features/Memos/Filter.swift | 26 +++++++++++++++ Memola/Features/Memos/MemosView.swift | 32 ++++++++++++++++--- Memola/Persistence/Objects/MemoObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 5 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 Memola/Features/Memos/Filter.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 131c62d..f2e3a84 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; }; EC1815082C2D980B00541369 /* Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815072C2D980B00541369 /* Sort.swift */; }; + EC18150A2C2DA09E00541369 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815092C2DA09E00541369 /* Filter.swift */; }; EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; }; EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; }; @@ -119,6 +120,7 @@ EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = ""; }; EC1815072C2D980B00541369 /* Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sort.swift; sourceTree = ""; }; + EC1815092C2DA09E00541369 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = ""; }; EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; @@ -414,6 +416,7 @@ children = ( ECA738792BE5EF0400A4542E /* MemosView.swift */, EC1815072C2D980B00541369 /* Sort.swift */, + EC1815092C2DA09E00541369 /* Filter.swift */, ); path = Memos; sourceTree = ""; @@ -869,6 +872,7 @@ files = ( ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */, + EC18150A2C2DA09E00541369 /* Filter.swift in Sources */, EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, diff --git a/Memola/Features/Memos/Filter.swift b/Memola/Features/Memos/Filter.swift new file mode 100644 index 0000000..4a874f5 --- /dev/null +++ b/Memola/Features/Memos/Filter.swift @@ -0,0 +1,26 @@ +// +// Filter.swift +// Memola +// +// Created by Dscyre Scotti on 6/27/24. +// + +import Foundation + +enum Filter: String, Identifiable, Hashable, Equatable { + var id: String { + rawValue + } + + case none + case favorites + + var name: String { + switch self { + case .none: return "All" + case .favorites: return "Favorites" + } + } + + static let all: [Filter] = [.none, .favorites] +} diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 9790539..53cec28 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -16,6 +16,7 @@ struct MemosView: View { @State var query: String = "" @AppStorage("memola.memo-objects.sort") var sort: Sort = .recent + @AppStorage("memola.memo-objects.filter") var filter: Filter = .none let cellWidth: CGFloat = 250 let cellHeight: CGFloat = 150 @@ -23,9 +24,14 @@ struct MemosView: View { init() { let standard = UserDefaults.standard var descriptors: [SortDescriptor] = [] + var predicate: NSPredicate? let sort = Sort(rawValue: standard.value(forKey: "memola.memo-objects.sort") as? String ?? "") ?? .recent + let filter = Filter(rawValue: standard.value(forKey: "memola.memo-objects.filter") as? String ?? "") ?? .none + if filter == .favorites { + predicate = NSPredicate(format: "isFavorite = YES") + } descriptors = sort.memoSortDescriptors - _memoObjects = FetchRequest(sortDescriptors: descriptors) + _memoObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) } var body: some View { @@ -53,6 +59,17 @@ struct MemosView: View { Image(systemName: "arrow.up.arrow.down.circle") } .hoverEffect(.lift) + Menu { + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + .pickerStyle(.automatic) + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + } } } } @@ -72,6 +89,13 @@ struct MemosView: View { .onChange(of: query) { oldValue, newValue in updatePredicate() } + .onChange(of: filter) { oldValue, newValue in + updatePredicate() + } + .onAppear { + memoObjects.sortDescriptors = sort.memoSortDescriptors + updatePredicate() + } } var memoGrid: some View { @@ -162,9 +186,9 @@ struct MemosView: View { if !query.isEmpty { predicates.append(NSPredicate(format: "title contains[c] %@", query)) } -// if filter == .favorites { -// predicates.append(NSPredicate(format: "isFavorite = YES")) -// } + if filter == .favorites { + predicates.append(NSPredicate(format: "isFavorite = YES")) + } memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index ad74aa9..724bc9e 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -14,6 +14,7 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var title: String @NSManaged var createdAt: Date @NSManaged var updatedAt: Date + @NSManaged var isFavorite: Bool @NSManaged var tool: ToolObject @NSManaged var canvas: CanvasObject } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 5e6773c..69dfb37 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -29,6 +29,7 @@ + From a10f8b562f22ddc8bc5ad5c5c94593b4e1410063 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 21:29:50 +0700 Subject: [PATCH 05/16] feat: add placeholder for empty state --- Memola.xcodeproj/project.pbxproj | 12 ++++ .../Views/Placeholder/Placeholder.swift | 56 +++++++++++++++++++ Memola/Features/Memos/MemosView.swift | 23 +++++--- 3 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 Memola/Components/Views/Placeholder/Placeholder.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index f2e3a84..bd344ef 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; }; EC1815082C2D980B00541369 /* Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815072C2D980B00541369 /* Sort.swift */; }; EC18150A2C2DA09E00541369 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815092C2DA09E00541369 /* Filter.swift */; }; + EC18150D2C2DAC3700541369 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC18150C2C2DAC3700541369 /* Placeholder.swift */; }; EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; }; EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; }; @@ -121,6 +122,7 @@ EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = ""; }; EC1815072C2D980B00541369 /* Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sort.swift; sourceTree = ""; }; EC1815092C2DA09E00541369 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; + EC18150C2C2DAC3700541369 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = ""; }; EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; @@ -251,9 +253,18 @@ path = ViewController; sourceTree = ""; }; + EC18150B2C2DA3AD00541369 /* Placeholder */ = { + isa = PBXGroup; + children = ( + EC18150C2C2DAC3700541369 /* Placeholder.swift */, + ); + path = Placeholder; + sourceTree = ""; + }; EC1B783A2BF9C68C005A34E2 /* Views */ = { isa = PBXGroup; children = ( + EC18150B2C2DA3AD00541369 /* Placeholder */, ECBE529B2C1D94A4006BDB3D /* CameraView */, ECFC51252BF8885000D0D051 /* ColorPicker */, ); @@ -973,6 +984,7 @@ ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */, ECA738972BE6014200A4542E /* Graphic.metal in Sources */, ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */, + EC18150D2C2DAC3700541369 /* Placeholder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Memola/Components/Views/Placeholder/Placeholder.swift b/Memola/Components/Views/Placeholder/Placeholder.swift new file mode 100644 index 0000000..035f11d --- /dev/null +++ b/Memola/Components/Views/Placeholder/Placeholder.swift @@ -0,0 +1,56 @@ +// +// Placeholder.swift +// Memola +// +// Created by Dscyre Scotti on 6/27/24. +// + +import SwiftUI + +struct Placeholder: View { + let info: Info + + var body: some View { + VStack(spacing: 15) { + Image(systemName: info.icon) + .font(.system(size: 50)) + .frame(width: 55, height: 55) + VStack(spacing: 3) { + Text(info.title) + .font(.title2) + .fontWeight(.bold) + .foregroundStyle(.primary) + Text(info.description) + .font(.callout) + .fontWeight(.regular) + .frame(minHeight: 50, alignment: .top) + } + } + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +extension Placeholder { + struct Info { + let title: String + let description: String + let icon: String + + static let memoNotFound: Info = { + let icon: String = "sparkle.magnifyingglass" + let title: String = "No Memos Found" + let description: String = "Explore your other memos or create your own." + return Placeholder.Info(title: title, description: description, icon: icon) + }() + + static let memoEmpty: Info = { + let icon: String = "wand.and.stars" + let title: String = "No Memos" + let description: String = "Create a new memo to jot your thoughts or notes down." + return Placeholder.Info(title: title, description: description, icon: icon) + }() + } +} diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 53cec28..92964f0 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -37,7 +37,7 @@ struct MemosView: View { var body: some View { NavigationStack { memoGrid - .navigationTitle("Memos") + .navigationTitle("Memola") .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) .toolbar { ToolbarItemGroup(placement: .primaryAction) { @@ -98,17 +98,22 @@ struct MemosView: View { } } + @ViewBuilder var memoGrid: some View { - GeometryReader { proxy in - let count = Int(proxy.size.width / cellWidth) - let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) - ScrollView { - LazyVGrid(columns: columns, spacing: 15) { - ForEach(memoObjects) { memo in - memoCard(memo) + if memoObjects.isEmpty { + Placeholder(info: query.isEmpty ? .memoEmpty : .memoNotFound) + } else { + GeometryReader { proxy in + let count = Int(proxy.size.width / cellWidth) + let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) + ScrollView { + LazyVGrid(columns: columns, spacing: 15) { + ForEach(memoObjects) { memo in + memoCard(memo) + } } + .padding() } - .padding() } } } From c75506c10d526ed417fe4886dc2a7efdf4c4ab0d Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 21:59:35 +0700 Subject: [PATCH 06/16] feat: add edit time display on memo card --- Memola.xcodeproj/project.pbxproj | 4 +++ .../Views/Placeholder/Placeholder.swift | 2 +- Memola/Extensions/Date++.swift | 32 +++++++++++++++++++ Memola/Features/Memos/MemosView.swift | 17 ++++++++-- 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 Memola/Extensions/Date++.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index bd344ef..c570ab5 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ EC1815082C2D980B00541369 /* Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815072C2D980B00541369 /* Sort.swift */; }; EC18150A2C2DA09E00541369 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1815092C2DA09E00541369 /* Filter.swift */; }; EC18150D2C2DAC3700541369 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC18150C2C2DAC3700541369 /* Placeholder.swift */; }; + EC18150F2C2DB13200541369 /* Date++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC18150E2C2DB13200541369 /* Date++.swift */; }; EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */; }; EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */; }; @@ -123,6 +124,7 @@ EC1815072C2D980B00541369 /* Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sort.swift; sourceTree = ""; }; EC1815092C2DA09E00541369 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; EC18150C2C2DAC3700541369 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; + EC18150E2C2DB13200541369 /* Date++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date++.swift"; sourceTree = ""; }; EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; EC2106AC2C10C2A700FBE27C /* AnyStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyStroke.swift; sourceTree = ""; }; EC2BEBF32C0F5FF7005DB0AF /* RTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTree.swift; sourceTree = ""; }; @@ -554,6 +556,7 @@ EC35655B2BF0712A00A4E0BF /* Float++.swift */, ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */, ECC995A42C1EB4CC00B2699A /* Data++.swift */, + EC18150E2C2DB13200541369 /* Date++.swift */, ); path = Extensions; sourceTree = ""; @@ -948,6 +951,7 @@ EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */, EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */, + EC18150F2C2DB13200541369 /* Date++.swift in Sources */, ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */, ECD12A932C1B062000B96E12 /* Photo.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, diff --git a/Memola/Components/Views/Placeholder/Placeholder.swift b/Memola/Components/Views/Placeholder/Placeholder.swift index 035f11d..c59a175 100644 --- a/Memola/Components/Views/Placeholder/Placeholder.swift +++ b/Memola/Components/Views/Placeholder/Placeholder.swift @@ -47,7 +47,7 @@ extension Placeholder { }() static let memoEmpty: Info = { - let icon: String = "wand.and.stars" + let icon: String = "note.text" let title: String = "No Memos" let description: String = "Create a new memo to jot your thoughts or notes down." return Placeholder.Info(title: title, description: description, icon: icon) diff --git a/Memola/Extensions/Date++.swift b/Memola/Extensions/Date++.swift new file mode 100644 index 0000000..2acaf6e --- /dev/null +++ b/Memola/Extensions/Date++.swift @@ -0,0 +1,32 @@ +// +// Date++.swift +// Memola +// +// Created by Dscyre Scotti on 6/27/24. +// + +import Foundation + +extension Date { + func getTimeDifference(to date: Date) -> String { + let calendar = Calendar.current + + let components = calendar.dateComponents([.minute, .hour, .day, .weekOfYear, .month, .year], from: self, to: date) + + if let years = components.year, years > 0 { + return "\(years) year\(years > 1 ? "s" : "") ago" + } else if let months = components.month, months > 0 { + return "\(months) month\(months > 1 ? "s" : "") ago" + } else if let weeks = components.weekOfYear, weeks > 0 { + return "\(weeks) week\(weeks > 1 ? "s" : "") ago" + } else if let days = components.day, days > 0 { + return "\(days) day\(days > 1 ? "s" : "") ago" + } else if let hours = components.hour, hours > 0 { + return "\(hours) hour\(hours > 1 ? "s" : "") ago" + } else if let minutes = components.minute, minutes > 0 { + return "\(minutes) minute\(minutes > 1 ? "s" : "") ago" + } else { + return "just now" + } + } +} diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 92964f0..63f162d 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -14,12 +14,14 @@ struct MemosView: View { @State var memo: MemoObject? @State var query: String = "" + @State var currentDate: Date = .now @AppStorage("memola.memo-objects.sort") var sort: Sort = .recent @AppStorage("memola.memo-objects.filter") var filter: Filter = .none let cellWidth: CGFloat = 250 let cellHeight: CGFloat = 150 + let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() init() { let standard = UserDefaults.standard @@ -92,6 +94,9 @@ struct MemosView: View { .onChange(of: filter) { oldValue, newValue in updatePredicate() } + .onReceive(timer) { date in + currentDate = date + } .onAppear { memoObjects.sortDescriptors = sort.memoSortDescriptors updatePredicate() @@ -123,9 +128,15 @@ struct MemosView: View { Rectangle() .frame(height: cellHeight) .clipShape(RoundedRectangle(cornerRadius: 10)) - Text(memoObject.title) - .font(.headline) - .fontWeight(.semibold) + VStack(alignment: .leading, spacing: 2) { + Text(memoObject.title) + .font(.headline) + .lineLimit(1) + .truncationMode(.tail) + Text("Edited \(memoObject.updatedAt.getTimeDifference(to: currentDate))") + .font(.caption) + .foregroundStyle(.secondary) + } } .onTapGesture { openMemo(for: memoObject) From 657fb3bf07c511240418458fbeb3d3d505ecb916 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 22:30:47 +0700 Subject: [PATCH 07/16] feat: adjust toolbar items --- Memola/Features/Memos/MemosView.swift | 87 ++++++++++++++++++++------- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 63f162d..e75fbb5 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -8,7 +8,8 @@ import SwiftUI struct MemosView: View { - @Environment(\.managedObjectContext) var managedObjectContext + @Environment(\.colorScheme) var colorScheme + @Environment(\.horizontalSizeClass) var horizontalSizeClass @FetchRequest var memoObjects: FetchedResults @@ -39,10 +40,14 @@ struct MemosView: View { var body: some View { NavigationStack { memoGrid - .navigationTitle("Memola") .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) .toolbar { - ToolbarItemGroup(placement: .primaryAction) { + ToolbarItem(placement: .topBarLeading) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) + } + ToolbarItemGroup(placement: .topBarTrailing) { HStack(spacing: 5) { Button { createMemo(title: "Untitled") @@ -50,27 +55,50 @@ struct MemosView: View { Image(systemName: "square.and.pencil") } .hoverEffect(.lift) - Menu { - Picker("", selection: $sort) { - ForEach(Sort.all) { sort in - Text(sort.name) - .tag(sort) + if horizontalSizeClass == .compact { + Menu { + VStack { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + .pickerStyle(.automatic) + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + .pickerStyle(.automatic) } + } label: { + Image(systemName: "ellipsis.circle") } - } label: { - Image(systemName: "arrow.up.arrow.down.circle") - } - .hoverEffect(.lift) - Menu { - Picker("", selection: $filter) { - ForEach(Filter.all) { filter in - Text(filter.name) - .tag(filter) + } else { + Menu { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + } + .hoverEffect(.lift) + Menu { + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + .pickerStyle(.automatic) + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") } - .pickerStyle(.automatic) - } label: { - Image(systemName: "line.3.horizontal.decrease.circle") } } } @@ -128,6 +156,23 @@ struct MemosView: View { Rectangle() .frame(height: cellHeight) .clipShape(RoundedRectangle(cornerRadius: 10)) + .overlay(alignment: .topTrailing) { + Image(systemName: memoObject.isFavorite ? "star.fill" : "star") + .foregroundStyle(memoObject.isFavorite ? .yellow : .white) + .frame(width: 20, height: 20) + .padding(5) + .background(.black.opacity(0.5)) + .cornerRadius(5) + .contentShape(Rectangle()) + .onTapGesture { + memoObject.isFavorite.toggle() + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } + .contentTransition(.symbolEffect(.replace)) + .padding(5) + } VStack(alignment: .leading, spacing: 2) { Text(memoObject.title) .font(.headline) @@ -149,7 +194,7 @@ struct MemosView: View { memoObject.createdAt = .now memoObject.updatedAt = .now - let canvasObject = CanvasObject(context: managedObjectContext) + let canvasObject = CanvasObject(\.viewContext) canvasObject.width = 8_000 canvasObject.height = 8_000 canvasObject.gridMode = 1 From 12fd0cd942f4d34b777b6b67d926b5aa7684adfe Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Thu, 27 Jun 2024 22:47:34 +0700 Subject: [PATCH 08/16] feat: add soft delete action --- Memola/Features/Memos/MemosView.swift | 38 +++++++++++++++---- Memola/Persistence/Objects/MemoObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index e75fbb5..bd2a982 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -27,13 +27,14 @@ struct MemosView: View { init() { let standard = UserDefaults.standard var descriptors: [SortDescriptor] = [] - var predicate: NSPredicate? + var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = NO")] let sort = Sort(rawValue: standard.value(forKey: "memola.memo-objects.sort") as? String ?? "") ?? .recent let filter = Filter(rawValue: standard.value(forKey: "memola.memo-objects.filter") as? String ?? "") ?? .none if filter == .favorites { - predicate = NSPredicate(format: "isFavorite = YES") + predicates.append(NSPredicate(format: "isFavorite = YES")) } descriptors = sort.memoSortDescriptors + let predicate = NSCompoundPredicate(type: .and, subpredicates: predicates) _memoObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) } @@ -156,6 +157,18 @@ struct MemosView: View { Rectangle() .frame(height: cellHeight) .clipShape(RoundedRectangle(cornerRadius: 10)) + .contextMenu { + Button { + openMemo(for: memoObject) + } label: { + Label("Open", systemImage: "doc.text") + } + Button(role: .destructive) { + markAsTrash(for: memoObject) + } label: { + Label("Delete", systemImage: "trash") + } + } .overlay(alignment: .topTrailing) { Image(systemName: memoObject.isFavorite ? "star.fill" : "star") .foregroundStyle(memoObject.isFavorite ? .yellow : .white) @@ -165,10 +178,7 @@ struct MemosView: View { .cornerRadius(5) .contentShape(Rectangle()) .onTapGesture { - memoObject.isFavorite.toggle() - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() - } + toggleFavorite(for: memoObject) } .contentTransition(.symbolEffect(.replace)) .padding(5) @@ -243,7 +253,7 @@ struct MemosView: View { } func updatePredicate() { - var predicates: [NSPredicate] = [] + var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = NO")] if !query.isEmpty { predicates.append(NSPredicate(format: "title contains[c] %@", query)) } @@ -252,4 +262,18 @@ struct MemosView: View { } memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } + + func toggleFavorite(for memo: MemoObject) { + memo.isFavorite.toggle() + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } + + func markAsTrash(for memo: MemoObject) { + memo.isTrash = true + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index 724bc9e..87ab58d 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -15,6 +15,7 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var createdAt: Date @NSManaged var updatedAt: Date @NSManaged var isFavorite: Bool + @NSManaged var isTrash: Bool @NSManaged var tool: ToolObject @NSManaged var canvas: CanvasObject } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 69dfb37..bf92615 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -30,6 +30,7 @@ + From c25d93e5435e8a098aa84d079c6f7bc58ae38dc0 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Fri, 28 Jun 2024 09:28:41 +0700 Subject: [PATCH 09/16] feat: update memo favorite button --- Memola/Features/Memos/MemosView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index bd2a982..52839fc 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -8,7 +8,6 @@ import SwiftUI struct MemosView: View { - @Environment(\.colorScheme) var colorScheme @Environment(\.horizontalSizeClass) var horizontalSizeClass @FetchRequest var memoObjects: FetchedResults @@ -171,16 +170,17 @@ struct MemosView: View { } .overlay(alignment: .topTrailing) { Image(systemName: memoObject.isFavorite ? "star.fill" : "star") - .foregroundStyle(memoObject.isFavorite ? .yellow : .white) + .contentTransition(.symbolEffect(.replace)) + .foregroundStyle(memoObject.isFavorite ? .yellow : .primary) + .animation(.easeInOut, value: memoObject.isFavorite) .frame(width: 20, height: 20) .padding(5) - .background(.black.opacity(0.5)) + .background(.gray) .cornerRadius(5) .contentShape(Rectangle()) .onTapGesture { toggleFavorite(for: memoObject) } - .contentTransition(.symbolEffect(.replace)) .padding(5) } VStack(alignment: .leading, spacing: 2) { From 63a619edf913faff9968c0c9d32c48c71c7b45fd Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 30 Jun 2024 00:31:56 +0700 Subject: [PATCH 10/16] feat: implement trash view --- Memola.xcodeproj/project.pbxproj | 80 +++++++- Memola/App/MemolaApp.swift | 2 +- .../Views/Placeholder/Placeholder.swift | 23 ++- .../Dashboard/Dashboard/DashboardView.swift | 29 +++ .../Details}/Memos/Filter.swift | 0 .../Details}/Memos/MemosView.swift | 189 ++++++++---------- .../{ => Dashboard/Details}/Memos/Sort.swift | 15 ++ .../Dashboard/Details/Shared/MemoCard.swift | 37 ++++ .../Dashboard/Details/Shared/MemoGrid.swift | 36 ++++ .../Details/Shared/MemoPreview.swift | 18 ++ .../Dashboard/Details/Trash/TrashView.swift | 85 ++++++++ .../Features/Dashboard/Sidebar/Sidebar.swift | 78 ++++++++ .../Dashboard/Sidebar/SidebarItem.swift | 35 ++++ Memola/Persistence/Objects/MemoObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 15 files changed, 520 insertions(+), 109 deletions(-) create mode 100644 Memola/Features/Dashboard/Dashboard/DashboardView.swift rename Memola/Features/{ => Dashboard/Details}/Memos/Filter.swift (100%) rename Memola/Features/{ => Dashboard/Details}/Memos/MemosView.swift (61%) rename Memola/Features/{ => Dashboard/Details}/Memos/Sort.swift (67%) create mode 100644 Memola/Features/Dashboard/Details/Shared/MemoCard.swift create mode 100644 Memola/Features/Dashboard/Details/Shared/MemoGrid.swift create mode 100644 Memola/Features/Dashboard/Details/Shared/MemoPreview.swift create mode 100644 Memola/Features/Dashboard/Details/Trash/TrashView.swift create mode 100644 Memola/Features/Dashboard/Sidebar/Sidebar.swift create mode 100644 Memola/Features/Dashboard/Sidebar/SidebarItem.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index c570ab5..44b43d2 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + EC01511E2C305CA9008A115E /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC01511D2C305CA9008A115E /* DashboardView.swift */; }; + EC0151202C305D7B008A115E /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC01511F2C305D7B008A115E /* SidebarItem.swift */; }; + EC0151232C306089008A115E /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0151222C306089008A115E /* Sidebar.swift */; }; + EC0151262C3067B9008A115E /* TrashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0151252C3067B9008A115E /* TrashView.swift */; }; + EC01512A2C306935008A115E /* MemoGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0151292C306935008A115E /* MemoGrid.swift */; }; + EC01512C2C306BEF008A115E /* MemoCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC01512B2C306BEF008A115E /* MemoCard.swift */; }; + EC01512E2C30727F008A115E /* MemoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC01512D2C30727F008A115E /* MemoPreview.swift */; }; EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; }; EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; }; EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; @@ -117,6 +124,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + EC01511D2C305CA9008A115E /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; + EC01511F2C305D7B008A115E /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; + EC0151222C306089008A115E /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; + EC0151252C3067B9008A115E /* TrashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashView.swift; sourceTree = ""; }; + EC0151292C306935008A115E /* MemoGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoGrid.swift; sourceTree = ""; }; + EC01512B2C306BEF008A115E /* MemoCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoCard.swift; sourceTree = ""; }; + EC01512D2C30727F008A115E /* MemoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoPreview.swift; sourceTree = ""; }; EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = ""; }; EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; @@ -239,6 +253,61 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + EC01511A2C305ABB008A115E /* Dashboard */ = { + isa = PBXGroup; + children = ( + EC0151272C306906008A115E /* Details */, + EC0151212C30605F008A115E /* Sidebar */, + EC01511C2C305C99008A115E /* Dashboard */, + ); + path = Dashboard; + sourceTree = ""; + }; + EC01511C2C305C99008A115E /* Dashboard */ = { + isa = PBXGroup; + children = ( + EC01511D2C305CA9008A115E /* DashboardView.swift */, + ); + path = Dashboard; + sourceTree = ""; + }; + EC0151212C30605F008A115E /* Sidebar */ = { + isa = PBXGroup; + children = ( + EC0151222C306089008A115E /* Sidebar.swift */, + EC01511F2C305D7B008A115E /* SidebarItem.swift */, + ); + path = Sidebar; + sourceTree = ""; + }; + EC0151242C3067B2008A115E /* Trash */ = { + isa = PBXGroup; + children = ( + EC0151252C3067B9008A115E /* TrashView.swift */, + ); + path = Trash; + sourceTree = ""; + }; + EC0151272C306906008A115E /* Details */ = { + isa = PBXGroup; + children = ( + EC0151282C306927008A115E /* Shared */, + EC0151242C3067B2008A115E /* Trash */, + ECA738782BE5EEF700A4542E /* Memos */, + ); + path = Details; + sourceTree = ""; + }; + EC0151282C306927008A115E /* Shared */ = { + isa = PBXGroup; + children = ( + EC0151292C306935008A115E /* MemoGrid.swift */, + EC01512B2C306BEF008A115E /* MemoCard.swift */, + EC01512D2C30727F008A115E /* MemoPreview.swift */, + ); + path = Shared; + sourceTree = ""; + }; EC1437B42BE748E60022C903 /* Views */ = { isa = PBXGroup; children = ( @@ -418,8 +487,8 @@ ECA738772BE5EEE800A4542E /* Features */ = { isa = PBXGroup; children = ( + EC01511A2C305ABB008A115E /* Dashboard */, ECA7387B2BE5EF3500A4542E /* Memo */, - ECA738782BE5EEF700A4542E /* Memos */, ); path = Features; sourceTree = ""; @@ -427,9 +496,9 @@ ECA738782BE5EEF700A4542E /* Memos */ = { isa = PBXGroup; children = ( - ECA738792BE5EF0400A4542E /* MemosView.swift */, EC1815072C2D980B00541369 /* Sort.swift */, EC1815092C2DA09E00541369 /* Filter.swift */, + ECA738792BE5EF0400A4542E /* MemosView.swift */, ); path = Memos; sourceTree = ""; @@ -897,6 +966,7 @@ ECA738912BE600F500A4542E /* Cache.metal in Sources */, ECA7389C2BE601AF00A4542E /* PointGridVertex.swift in Sources */, ECA738A82BE6025900A4542E /* GraphicUniforms.swift in Sources */, + EC01511E2C305CA9008A115E /* DashboardView.swift in Sources */, EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */, ECA738E62BE611FD00A4542E /* CGRect++.swift in Sources */, EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */, @@ -926,6 +996,7 @@ ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */, ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, + EC01512C2C306BEF008A115E /* MemoCard.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, ECA739082BE623F300A4542E /* PenDock.swift in Sources */, ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */, @@ -953,6 +1024,7 @@ EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */, EC18150F2C2DB13200541369 /* Date++.swift in Sources */, ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */, + EC0151232C306089008A115E /* Sidebar.swift in Sources */, ECD12A932C1B062000B96E12 /* Photo.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, EC2BEBF42C0F5FF7005DB0AF /* RTree.swift in Sources */, @@ -964,12 +1036,16 @@ ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */, ECA7389E2BE601CB00A4542E /* QuadVertex.swift in Sources */, ECA738B32BE60D9E00A4542E /* CanvasView.swift in Sources */, + EC0151202C305D7B008A115E /* SidebarItem.swift in Sources */, + EC01512A2C306935008A115E /* MemoGrid.swift in Sources */, ECA738C42BE60E8800A4542E /* MarkerPenStyle.swift in Sources */, + EC0151262C3067B9008A115E /* TrashView.swift in Sources */, ECA738BF2BE60E3400A4542E /* Pen.swift in Sources */, ECFA15282BEF225000455818 /* QuadObject.swift in Sources */, ECA738932BE6011100A4542E /* Stroke.metal in Sources */, ECA738B62BE60DCD00A4542E /* History.swift in Sources */, ECA738D22BE60F7B00A4542E /* PenStroke.swift in Sources */, + EC01512E2C30727F008A115E /* MemoPreview.swift in Sources */, ECA738F22BE6128F00A4542E /* Collection++.swift in Sources */, EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */, ECA738A32BE6020A00A4542E /* CGFloat++.swift in Sources */, diff --git a/Memola/App/MemolaApp.swift b/Memola/App/MemolaApp.swift index 7ee7826..c5aca74 100644 --- a/Memola/App/MemolaApp.swift +++ b/Memola/App/MemolaApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct MemolaApp: App { var body: some Scene { WindowGroup { - MemosView() + DashboardView() .persistence(\.viewContext) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification)) { _ in withPersistenceSync(\.viewContext) { context in diff --git a/Memola/Components/Views/Placeholder/Placeholder.swift b/Memola/Components/Views/Placeholder/Placeholder.swift index c59a175..c7b55ae 100644 --- a/Memola/Components/Views/Placeholder/Placeholder.swift +++ b/Memola/Components/Views/Placeholder/Placeholder.swift @@ -22,6 +22,7 @@ struct Placeholder: View { .foregroundStyle(.primary) Text(info.description) .font(.callout) + .lineLimit(.none) .fontWeight(.regular) .frame(minHeight: 50, alignment: .top) } @@ -41,15 +42,29 @@ extension Placeholder { static let memoNotFound: Info = { let icon: String = "sparkle.magnifyingglass" - let title: String = "No Memos Found" - let description: String = "Explore your other memos or create your own." + let title: String = "Memos Not Found" + let description: String = "There are no memos matching your search.\n Please try different keywords or create a new memo." return Placeholder.Info(title: title, description: description, icon: icon) }() static let memoEmpty: Info = { let icon: String = "note.text" - let title: String = "No Memos" - let description: String = "Create a new memo to jot your thoughts or notes down." + let title: String = "No Memos Available" + let description: String = "You have not created any memos yet.\n Tap the 'New Memo' button to get started." + return Placeholder.Info(title: title, description: description, icon: icon) + }() + + static let trashEmpty: Info = { + let icon: String = "trash" + let title: String = "Trash is Empty" + let description: String = "There are no memos in the trash.\n Deleted memos will appear here." + return Placeholder.Info(title: title, description: description, icon: icon) + }() + + static let trashNotFound: Info = { + let icon: String = "exclamationmark.magnifyingglass" + let title: String = "No Deleted Memos Found" + let description: String = "No memos found in the trash matching your search.\n Try different keywords or check back later." return Placeholder.Info(title: title, description: description, icon: icon) }() } diff --git a/Memola/Features/Dashboard/Dashboard/DashboardView.swift b/Memola/Features/Dashboard/Dashboard/DashboardView.swift new file mode 100644 index 0000000..ca9b15e --- /dev/null +++ b/Memola/Features/Dashboard/Dashboard/DashboardView.swift @@ -0,0 +1,29 @@ +// +// DashboardView.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct DashboardView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + @State var sidebarItem: SidebarItem? = .memos + + var body: some View { + NavigationSplitView { + Sidebar(sidebarItem: $sidebarItem, horizontalSizeClass: horizontalSizeClass) + } detail: { + switch sidebarItem { + case .memos: + MemosView() + case .trash: + TrashView() + default: + MemosView() + } + } + } +} diff --git a/Memola/Features/Memos/Filter.swift b/Memola/Features/Dashboard/Details/Memos/Filter.swift similarity index 100% rename from Memola/Features/Memos/Filter.swift rename to Memola/Features/Dashboard/Details/Memos/Filter.swift diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift similarity index 61% rename from Memola/Features/Memos/MemosView.swift rename to Memola/Features/Dashboard/Details/Memos/MemosView.swift index 52839fc..06b39d9 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -16,13 +16,15 @@ struct MemosView: View { @State var query: String = "" @State var currentDate: Date = .now - @AppStorage("memola.memo-objects.sort") var sort: Sort = .recent - @AppStorage("memola.memo-objects.filter") var filter: Filter = .none + @AppStorage("memola.memo-objects.memos.sort") var sort: Sort = .recent + @AppStorage("memola.memo-objects.memos.filter") var filter: Filter = .none - let cellWidth: CGFloat = 250 - let cellHeight: CGFloat = 150 let timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() + var placeholder: Placeholder.Info { + query.isEmpty ? .memoEmpty : .memoNotFound + } + init() { let standard = UserDefaults.standard var descriptors: [SortDescriptor] = [] @@ -38,57 +40,43 @@ struct MemosView: View { } var body: some View { - NavigationStack { - memoGrid - .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) - .toolbar { + MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in + memoCard(memoObject) + } + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + .toolbar { + if horizontalSizeClass == .compact { + ToolbarItem(placement: .principal) { + Text("Memos") + .font(.title3) + .fontWeight(.bold) + } + } else { ToolbarItem(placement: .topBarLeading) { Text("Memola") .font(.title3) .fontWeight(.bold) } - ToolbarItemGroup(placement: .topBarTrailing) { - HStack(spacing: 5) { - Button { - createMemo(title: "Untitled") - } label: { - Image(systemName: "square.and.pencil") - } - .hoverEffect(.lift) - if horizontalSizeClass == .compact { - Menu { - VStack { - Picker("", selection: $sort) { - ForEach(Sort.all) { sort in - Text(sort.name) - .tag(sort) - } - } - .pickerStyle(.automatic) - Picker("", selection: $filter) { - ForEach(Filter.all) { filter in - Text(filter.name) - .tag(filter) - } - } - .pickerStyle(.automatic) - } - } label: { - Image(systemName: "ellipsis.circle") - } - } else { - Menu { + } + ToolbarItemGroup(placement: .topBarTrailing) { + HStack(spacing: 5) { + Button { + createMemo(title: "Untitled") + } label: { + Image(systemName: "square.and.pencil") + } + .hoverEffect(.lift) + if horizontalSizeClass == .compact { + Menu { + VStack { Picker("", selection: $sort) { ForEach(Sort.all) { sort in Text(sort.name) .tag(sort) } } - } label: { - Image(systemName: "arrow.up.arrow.down.circle") - } - .hoverEffect(.lift) - Menu { + .pickerStyle(.automatic) Picker("", selection: $filter) { ForEach(Filter.all) { filter in Text(filter.name) @@ -96,66 +84,67 @@ struct MemosView: View { } } .pickerStyle(.automatic) - } label: { - Image(systemName: "line.3.horizontal.decrease.circle") } + } label: { + Image(systemName: "ellipsis.circle") + } + } else { + Menu { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + } + .hoverEffect(.lift) + Menu { + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + .pickerStyle(.automatic) + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") } } } } - } - .fullScreenCover(item: $memo) { memo in - MemoView(memo: memo) - .onDisappear { - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() - context.refreshAllObjects() - } - } - } - .onChange(of: sort) { oldValue, newValue in - memoObjects.sortDescriptors = newValue.memoSortDescriptors - } - .onChange(of: query) { oldValue, newValue in - updatePredicate() - } - .onChange(of: filter) { oldValue, newValue in - updatePredicate() - } - .onReceive(timer) { date in - currentDate = date - } - .onAppear { - memoObjects.sortDescriptors = sort.memoSortDescriptors - updatePredicate() - } - } - - @ViewBuilder - var memoGrid: some View { - if memoObjects.isEmpty { - Placeholder(info: query.isEmpty ? .memoEmpty : .memoNotFound) - } else { - GeometryReader { proxy in - let count = Int(proxy.size.width / cellWidth) - let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) - ScrollView { - LazyVGrid(columns: columns, spacing: 15) { - ForEach(memoObjects) { memo in - memoCard(memo) + } + .fullScreenCover(item: $memo) { memo in + MemoView(memo: memo) + .onDisappear { + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + context.refreshAllObjects() } } - .padding() - } } - } + .onChange(of: sort) { oldValue, newValue in + memoObjects.sortDescriptors = newValue.memoSortDescriptors + } + .onChange(of: query) { oldValue, newValue in + updatePredicate() + } + .onChange(of: filter) { oldValue, newValue in + updatePredicate() + } + .onReceive(timer) { date in + currentDate = date + } + .onAppear { + memoObjects.sortDescriptors = sort.memoSortDescriptors + updatePredicate() + } } func memoCard(_ memoObject: MemoObject) -> some View { - VStack(alignment: .leading, spacing: 5) { - Rectangle() - .frame(height: cellHeight) - .clipShape(RoundedRectangle(cornerRadius: 10)) + MemoCard(memoObject: memoObject) { card in + card .contextMenu { Button { openMemo(for: memoObject) @@ -183,15 +172,10 @@ struct MemosView: View { } .padding(5) } - VStack(alignment: .leading, spacing: 2) { - Text(memoObject.title) - .font(.headline) - .lineLimit(1) - .truncationMode(.tail) - Text("Edited \(memoObject.updatedAt.getTimeDifference(to: currentDate))") - .font(.caption) - .foregroundStyle(.secondary) - } + } details: { + Text("Edited \(memoObject.updatedAt.getTimeDifference(to: currentDate))") + .font(.caption) + .foregroundStyle(.secondary) } .onTapGesture { openMemo(for: memoObject) @@ -272,6 +256,7 @@ struct MemosView: View { func markAsTrash(for memo: MemoObject) { memo.isTrash = true + memo.deletedAt = .now withPersistence(\.viewContext) { context in try context.saveIfNeeded() } diff --git a/Memola/Features/Memos/Sort.swift b/Memola/Features/Dashboard/Details/Memos/Sort.swift similarity index 67% rename from Memola/Features/Memos/Sort.swift rename to Memola/Features/Dashboard/Details/Memos/Sort.swift index aa9f4b0..ddd4611 100644 --- a/Memola/Features/Memos/Sort.swift +++ b/Memola/Features/Dashboard/Details/Memos/Sort.swift @@ -46,4 +46,19 @@ extension Sort { return [SortDescriptor(\.createdAt)] } } + + var trashSortDescriptors: [SortDescriptor] { + switch self { + case .recent: + return [SortDescriptor(\.updatedAt, order: .reverse)] + case .aToZ: + return [SortDescriptor(\.title), SortDescriptor(\.updatedAt, order: .reverse)] + case .zToA: + return [SortDescriptor(\.title, order: .reverse), SortDescriptor(\.updatedAt, order: .reverse)] + case .newest: + return [SortDescriptor(\.createdAt, order: .reverse)] + case .oldest: + return [SortDescriptor(\.createdAt)] + } + } } diff --git a/Memola/Features/Dashboard/Details/Shared/MemoCard.swift b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift new file mode 100644 index 0000000..fb24331 --- /dev/null +++ b/Memola/Features/Dashboard/Details/Shared/MemoCard.swift @@ -0,0 +1,37 @@ +// +// MemoCard.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct MemoCard: View { + let memoObject: MemoObject + let modifyPreview: ((MemoPreview) -> Preview)? + let details: () -> Detail + + init(memoObject: MemoObject, @ViewBuilder modifyPreview: @escaping (MemoPreview) -> Preview, @ViewBuilder details: @escaping () -> Detail) { + self.memoObject = memoObject + self.modifyPreview = modifyPreview + self.details = details + } + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + if let modifyPreview { + modifyPreview(MemoPreview()) + } else { + MemoPreview() + } + VStack(alignment: .leading, spacing: 2) { + Text(memoObject.title) + .font(.headline) + .lineLimit(1) + .truncationMode(.tail) + details() + } + } + } +} diff --git a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift new file mode 100644 index 0000000..09fb23e --- /dev/null +++ b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift @@ -0,0 +1,36 @@ +// +// MemoGrid.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct MemoGrid: View { + let cellWidth: CGFloat = 250 + let cellHeight: CGFloat = 150 + + let memoObjects: FetchedResults + let placeholder: Placeholder.Info + @ViewBuilder let card: (MemoObject) -> Card + + var body: some View { + if memoObjects.isEmpty { + Placeholder(info: placeholder) + } else { + GeometryReader { proxy in + let count = Int(proxy.size.width / cellWidth) + let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 15), count: count) + ScrollView { + LazyVGrid(columns: columns, spacing: 15) { + ForEach(memoObjects) { memoObject in + card(memoObject) + } + } + .padding() + } + } + } + } +} diff --git a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift new file mode 100644 index 0000000..2d0418c --- /dev/null +++ b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift @@ -0,0 +1,18 @@ +// +// MemoPreview.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct MemoPreview: View { + let cellHeight: CGFloat = 150 + + var body: some View { + Rectangle() + .frame(height: cellHeight) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} diff --git a/Memola/Features/Dashboard/Details/Trash/TrashView.swift b/Memola/Features/Dashboard/Details/Trash/TrashView.swift new file mode 100644 index 0000000..6a010a5 --- /dev/null +++ b/Memola/Features/Dashboard/Details/Trash/TrashView.swift @@ -0,0 +1,85 @@ +// +// TrashView.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct TrashView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + @FetchRequest var memoObjects: FetchedResults + + @State var query: String = "" + + var placeholder: Placeholder.Info { + query.isEmpty ? .trashEmpty : .trashNotFound + } + + init() { + let descriptors = [SortDescriptor(\MemoObject.deletedAt, order: .reverse)] + let predicate = NSPredicate(format: "isTrash = YES") + _memoObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) + } + + var body: some View { + MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in + memoCard(memoObject) + } + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + .toolbar { + if horizontalSizeClass == .compact { + ToolbarItem(placement: .principal) { + Text("Trash") + .font(.title3) + .fontWeight(.bold) + } + } else { + ToolbarItem(placement: .topBarLeading) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) + } + } + } + .onChange(of: query) { oldValue, newValue in + updatePredicate() + } + } + + func memoCard(_ memoObject: MemoObject) -> some View { + MemoCard(memoObject: memoObject) { card in + card + .contextMenu { + Button { + + } label: { + Label("Restore", systemImage: "square.and.arrow.down") + } + Button(role: .destructive) { + + } label: { + Label("Delete Permanently", systemImage: "trash") + } + } + } details: { + Text("Deleted on \(memoObject.deletedAt.formatted(date: .abbreviated, time: .standard))") + .font(.caption) + .foregroundStyle(.secondary) + } + .onTapGesture { + + } + } + + func updatePredicate() { + var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = YES")] + if !query.isEmpty { + predicates.append(NSPredicate(format: "title contains[c] %@", query)) + } + memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) + } +} diff --git a/Memola/Features/Dashboard/Sidebar/Sidebar.swift b/Memola/Features/Dashboard/Sidebar/Sidebar.swift new file mode 100644 index 0000000..716bef3 --- /dev/null +++ b/Memola/Features/Dashboard/Sidebar/Sidebar.swift @@ -0,0 +1,78 @@ +// +// Sidebar.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import SwiftUI + +struct Sidebar: View { + let sidebarItems: [SidebarItem] = [.memos, .trash] + @Binding var sidebarItem: SidebarItem? + + let horizontalSizeClass: UserInterfaceSizeClass? + + var body: some View { + List(selection: $sidebarItem) { + ForEach(sidebarItems) { item in + Button { + sidebarItem = item + } label: { + Label(item.title, systemImage: item.icon) + .foregroundColor(.primary) + } + .buttonStyle(sidebarItem == item ? .selected : .unselected) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } + } + .listStyle(.sidebar) + .navigationTitle(horizontalSizeClass == .compact ? "Memola" : "") + .scrollContentBackground(.hidden) + .background(Color(uiColor: .secondarySystemFill)) + .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) + .navigationBarTitleDisplayMode(horizontalSizeClass == .compact ? .automatic : .inline) + } +} + +extension Sidebar { + struct SidebarItemButtonStyle: ButtonStyle { + let state: State + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(maxWidth: .infinity, alignment: .leading) + #if os(macOS) + .padding(10) + #else + .padding(.vertical, 8) + .padding(.horizontal, 10) + #endif + .contentShape(RoundedRectangle(cornerRadius: 10)) + .background { + switch state { + case .selected: + RoundedRectangle(cornerRadius: 10) + .fill(.primary.opacity(0.08)) + case .unselected: + EmptyView() + } + } + } + + enum State { + case selected + case unselected + } + } +} + +extension ButtonStyle where Self == Sidebar.SidebarItemButtonStyle { + static var selected: Sidebar.SidebarItemButtonStyle { + Sidebar.SidebarItemButtonStyle(state: .selected) + } + + static var unselected: Sidebar.SidebarItemButtonStyle { + Sidebar.SidebarItemButtonStyle(state: .unselected) + } +} diff --git a/Memola/Features/Dashboard/Sidebar/SidebarItem.swift b/Memola/Features/Dashboard/Sidebar/SidebarItem.swift new file mode 100644 index 0000000..7c62cad --- /dev/null +++ b/Memola/Features/Dashboard/Sidebar/SidebarItem.swift @@ -0,0 +1,35 @@ +// +// SidebarItem.swift +// Memola +// +// Created by Dscyre Scotti on 6/29/24. +// + +import Foundation + +enum SidebarItem: String, Identifiable, Hashable, Equatable { + var id: String { rawValue } + + case memos + case trash + + var title: String { + switch self { + case .memos: + "Memos" + case .trash: + "Trash" + } + } + + var icon: String { + switch self { + case .memos: + "rectangle.3.group" + case .trash: + "trash" + } + } + + static let all: [SidebarItem] = [.memos, .trash] +} diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index 87ab58d..0870cb2 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -14,6 +14,7 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var title: String @NSManaged var createdAt: Date @NSManaged var updatedAt: Date + @NSManaged var deletedAt: Date @NSManaged var isFavorite: Bool @NSManaged var isTrash: Bool @NSManaged var tool: ToolObject diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index bf92615..e043833 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -29,6 +29,7 @@ + From 542d3ea9d6664178eee7748d2f07562379ec012d Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 30 Jun 2024 01:02:37 +0700 Subject: [PATCH 11/16] feat: add restore and delete actions --- .../Dashboard/Dashboard/DashboardView.swift | 16 +++- .../Dashboard/Details/Memos/MemosView.swift | 15 +--- .../Dashboard/Details/Trash/TrashView.swift | 81 +++++++++++++++++-- Memola/Persistence/Objects/MemoObject.swift | 2 +- 4 files changed, 92 insertions(+), 22 deletions(-) diff --git a/Memola/Features/Dashboard/Dashboard/DashboardView.swift b/Memola/Features/Dashboard/Dashboard/DashboardView.swift index ca9b15e..c6aa549 100644 --- a/Memola/Features/Dashboard/Dashboard/DashboardView.swift +++ b/Memola/Features/Dashboard/Dashboard/DashboardView.swift @@ -10,6 +10,7 @@ import SwiftUI struct DashboardView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass + @State var memo: MemoObject? @State var sidebarItem: SidebarItem? = .memos var body: some View { @@ -18,12 +19,21 @@ struct DashboardView: View { } detail: { switch sidebarItem { case .memos: - MemosView() + MemosView(memo: $memo) case .trash: - TrashView() + TrashView(memo: $memo, sidebarItem: $sidebarItem) default: - MemosView() + MemosView(memo: $memo) } } + .fullScreenCover(item: $memo) { memo in + MemoView(memo: memo) + .onDisappear { + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + context.refreshAllObjects() + } + } + } } } diff --git a/Memola/Features/Dashboard/Details/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift index 06b39d9..d98119c 100644 --- a/Memola/Features/Dashboard/Details/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -12,10 +12,11 @@ struct MemosView: View { @FetchRequest var memoObjects: FetchedResults - @State var memo: MemoObject? @State var query: String = "" @State var currentDate: Date = .now + @Binding var memo: MemoObject? + @AppStorage("memola.memo-objects.memos.sort") var sort: Sort = .recent @AppStorage("memola.memo-objects.memos.filter") var filter: Filter = .none @@ -25,7 +26,8 @@ struct MemosView: View { query.isEmpty ? .memoEmpty : .memoNotFound } - init() { + init(memo: Binding) { + _memo = memo let standard = UserDefaults.standard var descriptors: [SortDescriptor] = [] var predicates: [NSPredicate] = [NSPredicate(format: "isTrash = NO")] @@ -115,15 +117,6 @@ struct MemosView: View { } } } - .fullScreenCover(item: $memo) { memo in - MemoView(memo: memo) - .onDisappear { - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() - context.refreshAllObjects() - } - } - } .onChange(of: sort) { oldValue, newValue in memoObjects.sortDescriptors = newValue.memoSortDescriptors } diff --git a/Memola/Features/Dashboard/Details/Trash/TrashView.swift b/Memola/Features/Dashboard/Details/Trash/TrashView.swift index 6a010a5..d161da7 100644 --- a/Memola/Features/Dashboard/Details/Trash/TrashView.swift +++ b/Memola/Features/Dashboard/Details/Trash/TrashView.swift @@ -13,18 +13,35 @@ struct TrashView: View { @FetchRequest var memoObjects: FetchedResults @State var query: String = "" + @State var restoredMemo: MemoObject? + @State var deletedMemo: MemoObject? + + @Binding var memo: MemoObject? + @Binding var sidebarItem: SidebarItem? var placeholder: Placeholder.Info { query.isEmpty ? .trashEmpty : .trashNotFound } - init() { + init(memo: Binding, sidebarItem: Binding) { + _memo = memo + _sidebarItem = sidebarItem let descriptors = [SortDescriptor(\MemoObject.deletedAt, order: .reverse)] let predicate = NSPredicate(format: "isTrash = YES") _memoObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) } var body: some View { + let restoresMemo = Binding { + restoredMemo != nil + } set: { _ in + restoredMemo = nil + } + let deletesMemo = Binding { + deletedMemo != nil + } set: { _ in + deletedMemo = nil + } MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in memoCard(memoObject) } @@ -48,6 +65,31 @@ struct TrashView: View { .onChange(of: query) { oldValue, newValue in updatePredicate() } + .alert("Restore Memo", isPresented: restoresMemo) { + Button { + restoreMemo(for: restoredMemo) + } label: { + Text("Restore") + } + Button { + restoreAndOpenMemo(for: restoredMemo) + } label: { + Text("Restore and Open") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Would you like to restore this memo or restore and open it?") + } + .alert("Delete Memo Permanently", isPresented: deletesMemo) { + Button(role: .destructive) { + deleteMemo(for: deletedMemo) + } label: { + Text("Delete") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Are you sure you want to permanently delete this memo? This action cannot be undone.") + } } func memoCard(_ memoObject: MemoObject) -> some View { @@ -55,23 +97,25 @@ struct TrashView: View { card .contextMenu { Button { - + restoreMemo(for: memoObject) } label: { Label("Restore", systemImage: "square.and.arrow.down") } Button(role: .destructive) { - + deletedMemo = memoObject } label: { Label("Delete Permanently", systemImage: "trash") } } } details: { - Text("Deleted on \(memoObject.deletedAt.formatted(date: .abbreviated, time: .standard))") - .font(.caption) - .foregroundStyle(.secondary) + if let deletedAt = memoObject.deletedAt { + Text("Deleted on \(deletedAt.formatted(date: .abbreviated, time: .standard))") + .font(.caption) + .foregroundStyle(.secondary) + } } .onTapGesture { - + restoredMemo = memoObject } } @@ -82,4 +126,27 @@ struct TrashView: View { } memoObjects.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } + + func restoreMemo(for memo: MemoObject?) { + guard let memo else { return } + memo.isTrash = false + memo.deletedAt = nil + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } + + func restoreAndOpenMemo(for memo: MemoObject?) { + restoreMemo(for: memo) + self.sidebarItem = .memos + self.memo = memo + } + + func deleteMemo(for memo: MemoObject?) { + guard let memo else { return } + withPersistenceSync(\.viewContext) { context in + context.delete(memo) + try context.saveIfNeeded() + } + } } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index 0870cb2..c62d8f4 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -14,7 +14,7 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var title: String @NSManaged var createdAt: Date @NSManaged var updatedAt: Date - @NSManaged var deletedAt: Date + @NSManaged var deletedAt: Date? @NSManaged var isFavorite: Bool @NSManaged var isTrash: Bool @NSManaged var tool: ToolObject From 53a0993ab3476a3a4616151cb9f44fa40309ef8f Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 30 Jun 2024 01:25:27 +0700 Subject: [PATCH 12/16] feat: fine tune navigation title --- .../Views/Placeholder/Placeholder.swift | 12 +- .../Dashboard/Details/Memos/MemosView.swift | 137 +++++++++--------- .../Dashboard/Details/Trash/TrashView.swift | 75 +++++----- .../Features/Dashboard/Sidebar/Sidebar.swift | 23 ++- 4 files changed, 124 insertions(+), 123 deletions(-) diff --git a/Memola/Components/Views/Placeholder/Placeholder.swift b/Memola/Components/Views/Placeholder/Placeholder.swift index c7b55ae..c2d2828 100644 --- a/Memola/Components/Views/Placeholder/Placeholder.swift +++ b/Memola/Components/Views/Placeholder/Placeholder.swift @@ -8,23 +8,25 @@ import SwiftUI struct Placeholder: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + let info: Info var body: some View { VStack(spacing: 15) { + let iconSize: CGFloat = horizontalSizeClass == .compact ? 40 : 50 Image(systemName: info.icon) - .font(.system(size: 50)) - .frame(width: 55, height: 55) + .font(.system(size: iconSize)) + .frame(width: iconSize * 1.1, height: iconSize * 1.1) VStack(spacing: 3) { Text(info.title) - .font(.title2) + .font(horizontalSizeClass == .compact ? .headline : .title2) .fontWeight(.bold) .foregroundStyle(.primary) Text(info.description) - .font(.callout) + .font(horizontalSizeClass == .compact ? .caption : .callout) .lineLimit(.none) .fontWeight(.regular) - .frame(minHeight: 50, alignment: .top) } } .foregroundStyle(.secondary) diff --git a/Memola/Features/Dashboard/Details/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift index d98119c..c931e3b 100644 --- a/Memola/Features/Dashboard/Details/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -43,66 +43,37 @@ struct MemosView: View { var body: some View { MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in - memoCard(memoObject) - } - .navigationBarTitleDisplayMode(.inline) - .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) - .toolbar { - if horizontalSizeClass == .compact { - ToolbarItem(placement: .principal) { - Text("Memos") - .font(.title3) - .fontWeight(.bold) - } - } else { - ToolbarItem(placement: .topBarLeading) { - Text("Memola") - .font(.title3) - .fontWeight(.bold) - } + memoCard(memoObject) + } + .navigationTitle(horizontalSizeClass == .compact ? "Memos" : "") + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + .toolbar { + if horizontalSizeClass == .regular { + ToolbarItem(placement: .topBarLeading) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) } - ToolbarItemGroup(placement: .topBarTrailing) { - HStack(spacing: 5) { - Button { - createMemo(title: "Untitled") - } label: { - Image(systemName: "square.and.pencil") - } - .hoverEffect(.lift) - if horizontalSizeClass == .compact { - Menu { - VStack { - Picker("", selection: $sort) { - ForEach(Sort.all) { sort in - Text(sort.name) - .tag(sort) - } - } - .pickerStyle(.automatic) - Picker("", selection: $filter) { - ForEach(Filter.all) { filter in - Text(filter.name) - .tag(filter) - } - } - .pickerStyle(.automatic) - } - } label: { - Image(systemName: "ellipsis.circle") - } - } else { - Menu { + } + ToolbarItemGroup(placement: .topBarTrailing) { + HStack(spacing: 5) { + Button { + createMemo(title: "Untitled") + } label: { + Image(systemName: "square.and.pencil") + } + .hoverEffect(.lift) + if horizontalSizeClass == .compact { + Menu { + VStack { Picker("", selection: $sort) { ForEach(Sort.all) { sort in Text(sort.name) .tag(sort) } } - } label: { - Image(systemName: "arrow.up.arrow.down.circle") - } - .hoverEffect(.lift) - Menu { + .pickerStyle(.automatic) Picker("", selection: $filter) { ForEach(Filter.all) { filter in Text(filter.name) @@ -110,29 +81,53 @@ struct MemosView: View { } } .pickerStyle(.automatic) - } label: { - Image(systemName: "line.3.horizontal.decrease.circle") } + } label: { + Image(systemName: "ellipsis.circle") + } + } else { + Menu { + Picker("", selection: $sort) { + ForEach(Sort.all) { sort in + Text(sort.name) + .tag(sort) + } + } + } label: { + Image(systemName: "arrow.up.arrow.down.circle") + } + .hoverEffect(.lift) + Menu { + Picker("", selection: $filter) { + ForEach(Filter.all) { filter in + Text(filter.name) + .tag(filter) + } + } + .pickerStyle(.automatic) + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") } } } } - .onChange(of: sort) { oldValue, newValue in - memoObjects.sortDescriptors = newValue.memoSortDescriptors - } - .onChange(of: query) { oldValue, newValue in - updatePredicate() - } - .onChange(of: filter) { oldValue, newValue in - updatePredicate() - } - .onReceive(timer) { date in - currentDate = date - } - .onAppear { - memoObjects.sortDescriptors = sort.memoSortDescriptors - updatePredicate() - } + } + .onChange(of: sort) { oldValue, newValue in + memoObjects.sortDescriptors = newValue.memoSortDescriptors + } + .onChange(of: query) { oldValue, newValue in + updatePredicate() + } + .onChange(of: filter) { oldValue, newValue in + updatePredicate() + } + .onReceive(timer) { date in + currentDate = date + } + .onAppear { + memoObjects.sortDescriptors = sort.memoSortDescriptors + updatePredicate() + } } func memoCard(_ memoObject: MemoObject) -> some View { diff --git a/Memola/Features/Dashboard/Details/Trash/TrashView.swift b/Memola/Features/Dashboard/Details/Trash/TrashView.swift index d161da7..5deff1a 100644 --- a/Memola/Features/Dashboard/Details/Trash/TrashView.swift +++ b/Memola/Features/Dashboard/Details/Trash/TrashView.swift @@ -45,51 +45,46 @@ struct TrashView: View { MemoGrid(memoObjects: memoObjects, placeholder: placeholder) { memoObject in memoCard(memoObject) } - .navigationBarTitleDisplayMode(.inline) - .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) - .toolbar { - if horizontalSizeClass == .compact { - ToolbarItem(placement: .principal) { - Text("Trash") - .font(.title3) - .fontWeight(.bold) - } - } else { - ToolbarItem(placement: .topBarLeading) { - Text("Memola") - .font(.title3) - .fontWeight(.bold) - } + .navigationTitle(horizontalSizeClass == .compact ? "Trash" : "") + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $query, placement: .toolbar, prompt: Text("Search")) + .toolbar { + if horizontalSizeClass == .regular { + ToolbarItem(placement: .topBarLeading) { + Text("Memola") + .font(.title3) + .fontWeight(.bold) } } - .onChange(of: query) { oldValue, newValue in - updatePredicate() + } + .onChange(of: query) { oldValue, newValue in + updatePredicate() + } + .alert("Restore Memo", isPresented: restoresMemo) { + Button { + restoreMemo(for: restoredMemo) + } label: { + Text("Restore") } - .alert("Restore Memo", isPresented: restoresMemo) { - Button { - restoreMemo(for: restoredMemo) - } label: { - Text("Restore") - } - Button { - restoreAndOpenMemo(for: restoredMemo) - } label: { - Text("Restore and Open") - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Would you like to restore this memo or restore and open it?") + Button { + restoreAndOpenMemo(for: restoredMemo) + } label: { + Text("Restore and Open") } - .alert("Delete Memo Permanently", isPresented: deletesMemo) { - Button(role: .destructive) { - deleteMemo(for: deletedMemo) - } label: { - Text("Delete") - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Are you sure you want to permanently delete this memo? This action cannot be undone.") + Button("Cancel", role: .cancel) { } + } message: { + Text("Would you like to restore this memo or restore and open it?") + } + .alert("Delete Memo Permanently", isPresented: deletesMemo) { + Button(role: .destructive) { + deleteMemo(for: deletedMemo) + } label: { + Text("Delete") } + Button("Cancel", role: .cancel) { } + } message: { + Text("Are you sure you want to permanently delete this memo? This action cannot be undone.") + } } func memoCard(_ memoObject: MemoObject) -> some View { diff --git a/Memola/Features/Dashboard/Sidebar/Sidebar.swift b/Memola/Features/Dashboard/Sidebar/Sidebar.swift index 716bef3..99ba758 100644 --- a/Memola/Features/Dashboard/Sidebar/Sidebar.swift +++ b/Memola/Features/Dashboard/Sidebar/Sidebar.swift @@ -16,14 +16,23 @@ struct Sidebar: View { var body: some View { List(selection: $sidebarItem) { ForEach(sidebarItems) { item in - Button { - sidebarItem = item - } label: { - Label(item.title, systemImage: item.icon) - .foregroundColor(.primary) + if horizontalSizeClass == .compact { + Button { + sidebarItem = item + } label: { + Label(item.title, systemImage: item.icon) + .foregroundColor(.primary) + } + } else { + Button { + sidebarItem = item + } label: { + Label(item.title, systemImage: item.icon) + .foregroundColor(.primary) + } + .buttonStyle(sidebarItem == item ? .selected : .unselected) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } - .buttonStyle(sidebarItem == item ? .selected : .unselected) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } } .listStyle(.sidebar) From ecb89e6afb9cf558674f98b297cd42cc1da59a35 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 30 Jun 2024 17:42:51 +0700 Subject: [PATCH 13/16] feat: adjust cell size --- .../Features/Dashboard/Details/Shared/MemoGrid.swift | 11 ++++++++--- .../Dashboard/Details/Shared/MemoPreview.swift | 9 ++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift index 09fb23e..e443f53 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoGrid.swift @@ -8,13 +8,18 @@ import SwiftUI struct MemoGrid: View { - let cellWidth: CGFloat = 250 - let cellHeight: CGFloat = 150 - + @Environment(\.horizontalSizeClass) var horizontalSizeClass let memoObjects: FetchedResults let placeholder: Placeholder.Info @ViewBuilder let card: (MemoObject) -> Card + var cellWidth: CGFloat { + if horizontalSizeClass == .compact { + return 180 + } + return 250 + } + var body: some View { if memoObjects.isEmpty { Placeholder(info: placeholder) diff --git a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift index 2d0418c..6ec7f01 100644 --- a/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift +++ b/Memola/Features/Dashboard/Details/Shared/MemoPreview.swift @@ -8,7 +8,14 @@ import SwiftUI struct MemoPreview: View { - let cellHeight: CGFloat = 150 + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + var cellHeight: CGFloat { + if horizontalSizeClass == .compact { + return 120 + } + return 150 + } var body: some View { Rectangle() From cf0805010361a2cca3fac4523fa67f7fbbc415b7 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Tue, 2 Jul 2024 02:37:37 +0700 Subject: [PATCH 14/16] feat: fine tune memo view toolbar for pen element --- Memola.xcodeproj/project.pbxproj | 12 + Memola/Canvas/Tool/Pen/Core/PenStyle.swift | 1 + .../Tool/Pen/PenStyles/EraserPenStyle.swift | 2 + .../Tool/Pen/PenStyles/MarkerPenStyle.swift | 2 + .../Memo/ElementToolbar/ElementToolbar.swift | 240 ++++++++++++++ Memola/Features/Memo/Memo/MemoView.swift | 65 +++- Memola/Features/Memo/PenDock/PenDock.swift | 294 ++++++++++++++++-- Memola/Features/Memo/Toolbar/Toolbar.swift | 173 +---------- .../eraser-compact.imageset/Contents.json | 23 ++ .../eraser/eraser-compact.imageset/eraser.png | Bin 0 -> 4287 bytes .../eraser-compact.imageset/eraser@2x.png | Bin 0 -> 14893 bytes .../eraser-compact.imageset/eraser@3x.png | Bin 0 -> 29736 bytes .../Contents.json | 23 ++ .../marker-base.png | Bin 0 -> 4412 bytes .../marker-base@2x.png | Bin 0 -> 12976 bytes .../marker-base@3x.png | Bin 0 -> 26591 bytes .../marker-tip-compact.imageset/Contents.json | 23 ++ .../marker-tip.png | Bin 0 -> 440 bytes .../marker-tip@2x.png | Bin 0 -> 904 bytes .../marker-tip@3x.png | Bin 0 -> 1452 bytes 20 files changed, 646 insertions(+), 212 deletions(-) create mode 100644 Memola/Features/Memo/ElementToolbar/ElementToolbar.swift create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/Contents.json create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/eraser.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/eraser@2x.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/eraser@3x.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/Contents.json create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/marker-base.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/marker-base@2x.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/marker-base@3x.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/Contents.json create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@2x.png create mode 100644 Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@3x.png diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 44b43d2..55311f8 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */; }; ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; }; ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; }; + ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */; }; ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; }; ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; }; ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; }; @@ -230,6 +231,7 @@ ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRenderPass.swift; sourceTree = ""; }; ECD12A922C1B062000B96E12 /* Photo.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Photo.metal; sourceTree = ""; }; ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = ""; }; + ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToolbar.swift; sourceTree = ""; }; ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = ""; }; ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = ""; }; ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = ""; }; @@ -506,6 +508,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + ECDAC0792C318DAF0000ED77 /* ElementToolbar */, ECBE52942C1D58F5006BDB3D /* PhotoPreview */, EC1B783B2BFA0AAC005A34E2 /* Toolbar */, EC5050082BF65D0500B4D86E /* Memo */, @@ -834,6 +837,14 @@ path = Photo; sourceTree = ""; }; + ECDAC0792C318DAF0000ED77 /* ElementToolbar */ = { + isa = PBXGroup; + children = ( + ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */, + ); + path = ElementToolbar; + sourceTree = ""; + }; ECE883B82C009DC30045C53D /* Strokes */ = { isa = PBXGroup; children = ( @@ -1031,6 +1042,7 @@ ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */, ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */, EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */, + ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */, EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */, EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */, ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */, diff --git a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift index 8bf961a..6a9edbd 100644 --- a/Memola/Canvas/Tool/Pen/Core/PenStyle.swift +++ b/Memola/Canvas/Tool/Pen/Core/PenStyle.swift @@ -10,6 +10,7 @@ import Foundation protocol PenStyle { var icon: (base: String, tip: String?) { get } + var compactIcon: (base: String, tip: String?) { get } var textureName: String? { get } var thickness: (min: CGFloat, max: CGFloat) { get } var thicknessSteps: [CGFloat] { get } diff --git a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift index f4e96b6..906da73 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/EraserPenStyle.swift @@ -10,6 +10,8 @@ import Foundation struct EraserPenStyle: PenStyle { var icon: (base: String, tip: String?) = ("eraser", nil) + var compactIcon: (base: String, tip: String?) = ("eraser-compact", nil) + var textureName: String? = nil var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30) diff --git a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift index 2e608a4..c171cac 100644 --- a/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift +++ b/Memola/Canvas/Tool/Pen/PenStyles/MarkerPenStyle.swift @@ -10,6 +10,8 @@ import Foundation struct MarkerPenStyle: PenStyle { var icon: (base: String, tip: String?) = ("marker-base", "marker-tip") + var compactIcon: (base: String, tip: String?) = ("marker-base-compact", "marker-tip-compact") + var textureName: String? = "point-texture" var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30) diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift new file mode 100644 index 0000000..7db9ff4 --- /dev/null +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -0,0 +1,240 @@ +// +// ElementToolbar.swift +// Memola +// +// Created by Dscyre Scotti on 6/30/24. +// + +import SwiftUI +import PhotosUI +import AVFoundation + +struct ElementToolbar: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + let size: CGFloat + @ObservedObject var tool: Tool + @ObservedObject var canvas: Canvas + + @State var opensCamera: Bool = false + @State var isCameraAccessDenied: Bool = false + @State var photosPickerItem: PhotosPickerItem? + + @Namespace var namespace + + var body: some View { + Group { + if horizontalSizeClass == .regular { + regularToolbar + } else { + ZStack(alignment: .bottomLeading) { + compactToolbar + if tool.selection == .photo { + photoOption + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .frame(maxWidth: .infinity) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + } + } + } + .fullScreenCover(isPresented: $opensCamera) { + let image: Binding = Binding { + tool.selectedPhotoItem?.image + } set: { image in + guard let image else { return } + tool.selectPhoto(image, for: canvas.canvasID) + } + CameraView(image: image, canvas: canvas) + .ignoresSafeArea() + } + .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { + Button { + if let url = URL(string: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { + UIApplication.shared.open(url) + } + } label: { + Text("Open Settings") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") + } + .onChange(of: photosPickerItem) { oldValue, newValue in + if newValue != nil { + Task { + tool.isLoadingPhoto = true + let data = try? await newValue?.loadTransferable(type: Data.self) + if let data, let image = UIImage(data: data) { + tool.selectPhoto(image, for: canvas.canvasID) + } + photosPickerItem = nil + } + } + } + } + + var regularToolbar: some View { + HStack(spacing: 0) { + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "hand.draw.fill") + .fontWeight(.heavy) + .contentShape(.circle) + .frame(width: size, height: size) + .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .background { + if tool.selection == .hand { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + } + Button { + withAnimation { + tool.selectTool(.pen) + } + } label: { + Image(systemName: "pencil") + .fontWeight(.heavy) + .contentShape(.circle) + .frame(width: size, height: size) + .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .background { + if tool.selection == .pen { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + } + HStack(spacing: 0) { + Button { + withAnimation { + tool.selectTool(.photo) + } + } label: { + Image(systemName: "photo") + .contentShape(.circle) + .frame(width: size, height: size) + .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .background { + if tool.selection == .photo { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + if tool.selection != .photo { + Color.clear + .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) + } + } + if tool.selection == .photo { + photoOption + .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) + .transition(.blurReplace.animation(.easeIn(duration: 0.1))) + } + } + .background { + if tool.selection == .photo { + RoundedRectangle(cornerRadius: 8) + .fill(Color.white.tertiary) + .transition(.move(edge: .leading).combined(with: .opacity).animation(.easeIn(duration: 0.1))) + } + } + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .transition(.move(edge: .top).combined(with: .blurReplace)) + } + + var compactToolbar: some View { + HStack(spacing: 0) { + Button { + withAnimation { + tool.selectTool(.pen) + } + } label: { + Image(systemName: "pencil") + .fontWeight(.heavy) + .contentShape(.circle) + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + Button { + withAnimation { + tool.selectTool(.photo) + } + } label: { + Image(systemName: "photo") + .contentShape(.circle) + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + .padding(10) + } + + var photoOption: some View { + HStack(spacing: 0) { + Button { + openCamera() + } label: { + Image(systemName: "camera.fill") + .contentShape(.circle) + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + Image(systemName: "photo.fill.on.rectangle.fill") + .contentShape(.circle) + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + } + } + + func openCamera() { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { status in + withAnimation { + if status { + opensCamera = true + } else { + isCameraAccessDenied = true + } + } + } + case .authorized: + opensCamera = true + default: + isCameraAccessDenied = true + } + } +} diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 4a7a384..5fd21d8 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -9,6 +9,8 @@ import SwiftUI import CoreData struct MemoView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @StateObject var tool: Tool @StateObject var canvas: Canvas @StateObject var history: History @@ -28,6 +30,36 @@ struct MemoView: View { } var body: some View { + Group { + if horizontalSizeClass == .regular { + canvasView + } else { + compactCanvasView + } + } + .overlay(alignment: .top) { + Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) + } + .disabled(textFieldState || tool.isLoadingPhoto) + .disabled(canvas.state == .loading || canvas.state == .closing) + .overlay { + switch canvas.state { + case .loading: + loadingIndicator("Loading memo...") + case .closing: + loadingIndicator("Saving memo...") + default: + EmptyView() + } + } + .overlay { + if tool.isLoadingPhoto { + loadingIndicator("Loading photo...") + } + } + } + + var canvasView: some View { CanvasView(tool: tool, canvas: canvas, history: history) .ignoresSafeArea() .overlay(alignment: .bottomTrailing) { @@ -47,24 +79,29 @@ struct MemoView: View { .overlay(alignment: .bottomLeading) { zoomControl } - .disabled(textFieldState || tool.isLoadingPhoto) - .overlay(alignment: .top) { - Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) - } - .disabled(canvas.state == .loading || canvas.state == .closing) - .overlay { - switch canvas.state { - case .loading: - loadingIndicator("Loading memo...") - case .closing: - loadingIndicator("Saving memo...") + } + + var compactCanvasView: some View { + CanvasView(tool: tool, canvas: canvas, history: history) + .ignoresSafeArea() + .overlay(alignment: .bottom) { + switch tool.selection { + case .pen: + PenDock(tool: tool, canvas: canvas, size: size) + .transition(.move(edge: .bottom)) + case .photo: + if let photoItem = tool.selectedPhotoItem { + PhotoPreview(photoItem: photoItem, tool: tool) + .transition(.move(edge: .trailing)) + } default: EmptyView() } } - .overlay { - if tool.isLoadingPhoto { - loadingIndicator("Loading photo...") + .overlay(alignment: .bottom) { + if tool.selection == .hand { + ElementToolbar(size: size, tool: tool, canvas: canvas) + .transition(.move(edge: .bottom)) } } } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index d9af8ca..52ddbee 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -8,41 +8,97 @@ import SwiftUI struct PenDock: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject var tool: Tool @ObservedObject var canvas: Canvas let size: CGFloat - let width: CGFloat = 90 - let height: CGFloat = 30 - let factor: CGFloat = 0.9 + var width: CGFloat { + horizontalSizeClass == .compact ? 30 : 90 + } + var height: CGFloat { + horizontalSizeClass == .compact ? 90 : 30 + } + var factor: CGFloat { + horizontalSizeClass == .compact ? 0.9 : 0.9 + } @State var refreshScrollId: UUID = UUID() @State var opensColorPicker: Bool = false var body: some View { - ZStack(alignment: .bottomTrailing) { - if !canvas.locksCanvas { - VStack(alignment: .trailing) { - penPropertyTool - penItemList + if horizontalSizeClass == .regular { + ZStack(alignment: .bottomTrailing) { + if !canvas.locksCanvas { + VStack(alignment: .trailing) { + penPropertyTool + penItemList + } + .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } - .fixedSize() - .frame(maxHeight: .infinity) - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + lockButton + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + } else { + ZStack(alignment: .bottomTrailing) { + if !canvas.locksCanvas { + GeometryReader { proxy in + HStack(alignment: .bottom, spacing: 10) { + newPenButton + .frame(height: height * factor - 18) + compactPenItemList + .fixedSize(horizontal: false, vertical: true) + compactPenPropertyTool + } + .padding(.horizontal, 10) + .clipped() + .background(alignment: .bottom) { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + .frame(height: height * factor - 18) + } + .padding([.horizontal, .bottom], 10) + .frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom) + .frame(maxWidth: .infinity) + } + .overlay(alignment: .bottom) { + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "chevron.compact.down") + .font(.headline) + .frame(width: 80) + .padding(10) + .background(.regularMaterial) + .clipShape(.capsule) + .contentShape(.capsule) + } + .offset(y: 5) + } + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + lockButton + .frame(maxWidth: .infinity, alignment: .bottomTrailing) + .padding(10) + .offset(y: canvas.locksCanvas ? 0 : -(height * factor - size + 20)) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } - lockButton - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) } } + @ViewBuilder var penItemList: some View { ScrollViewReader { proxy in ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: 0) { ForEach(tool.pens) { pen in - penItemRow(pen) + penItem(pen) .id(pen.id) .scrollTransition { content, phase in content @@ -75,7 +131,34 @@ struct PenDock: View { } } - func penItemRow(_ pen: Pen) -> some View { + @ViewBuilder + var compactPenItemList: some View { + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 0) { + ForEach(tool.pens) { pen in + compactPenItem(pen) + .id(pen.id) + .scrollTransition { content, phase in + content + .scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .bottom) + } + } + } + .padding(.horizontal, 10) + .id(refreshScrollId) + } + .onReceive(tool.scrollPublisher) { id in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + proxy.scrollTo(id) + } + } + } + } + } + + func penItem(_ pen: Pen) -> some View { ZStack { penShadow(pen) if let tip = pen.style.icon.tip { @@ -88,7 +171,7 @@ struct PenDock: View { .resizable() } .frame(width: width * factor, height: height * factor) - .padding(.vertical, 5) + .padding(.horizontal, 5) .contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) .onTapGesture { if tool.selectedPen !== pen { @@ -148,6 +231,79 @@ struct PenDock: View { .offset(x: tool.selectedPen === pen ? 0 : 25) } + func compactPenItem(_ pen: Pen) -> some View { + ZStack { + compactPenShadow(pen) + if let tip = pen.style.compactIcon.tip { + Image(tip) + .resizable() + .renderingMode(.template) + .foregroundStyle(Color.rgba(from: pen.rgba)) + } + Image(pen.style.compactIcon.base) + .resizable() + } + .frame(width: width * factor, height: height * factor) + .padding(.top, 5) + .contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10))) + .onTapGesture { + if tool.selectedPen !== pen { + tool.selectPen(pen) + } + } + .padding(.horizontal, 10) + .contextMenu(if: pen.strokeStyle != .eraser) { + ControlGroup { + 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) + } + .controlGroupStyle(.menu) + } preview: { + penPreview(pen) + .drawingGroup() + .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) + } + .onDrag(if: pen.strokeStyle != .eraser) { + tool.draggedPen = pen + return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider() + } preview: { + penPreview(pen) + .contentShape(.dragPreview, .rect(cornerRadius: 10)) + } + .onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() })) + .offset(y: tool.selectedPen === pen ? 0 : 25) + } + @ViewBuilder var penPropertyTool: some View { if let pen = tool.selectedPen { @@ -170,6 +326,23 @@ struct PenDock: View { } } + @ViewBuilder + var compactPenPropertyTool: some View { + if let pen = tool.selectedPen { + HStack(spacing: 10) { + compactPenThicknessPicker(pen) + .frame(width: width) + .rotationEffect(.degrees(-90)) + if pen.strokeStyle == .marker { + penColorPicker(pen) + .frame(width: width) + .transition(.move(edge: .trailing).combined(with: .opacity)) + } + } + .frame(height: height * factor - 18) + } + } + func penColorPicker(_ pen: Pen) -> some View { Button { opensColorPicker = true @@ -191,12 +364,13 @@ struct PenDock: View { } .background(baseColor) .clipShape(.rect(cornerRadius: 8)) - .frame(height: 25) + .frame(height: horizontalSizeClass == .compact ? 30 : 25) .overlay { RoundedRectangle(cornerRadius: 8) .stroke(Color.gray, lineWidth: 0.4) } .padding(0.2) + .drawingGroup() } .buttonStyle(.plain) .hoverEffect(.lift) @@ -250,19 +424,41 @@ struct PenDock: View { } } + @ViewBuilder + func compactPenThicknessPicker(_ pen: Pen) -> some View { + let minimum: CGFloat = pen.style.thickness.min + let maximum: CGFloat = pen.style.thickness.max + let start: CGFloat = 4 + let end: CGFloat = 7 + let selection = Binding( + get: { pen.thickness }, + set: { + pen.thickness = $0 + tool.objectWillChange.send() + } + ) + Picker("", selection: selection) { + ForEach(pen.style.thicknessSteps, id: \.self) { step in + let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) + Circle() + .fill(.black) + .frame(width: size, height: size) + .frame(width: size + 2, height: size + 2) + } + } + .hoverEffect(.lift) + .pickerStyle(.wheel) + .frame(width: 50, height: 30) + .onChange(of: pen.thickness) { _, _ in + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } + } + var newPenButton: some View { Button { - let pen = PenObject.createObject(\.viewContext, penStyle: .marker) - var selectedPen = tool.selectedPen - selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last) - if let color = selectedPen?.rgba { - pen.color = color - } - pen.isSelected = true - pen.tool = tool.object - pen.orderIndex = Int16(tool.pens.count) - let _pen = Pen(object: pen) - tool.addPen(_pen) + createNewPen() } label: { Image(systemName: "plus.circle.fill") .font(.title2) @@ -317,6 +513,30 @@ struct PenDock: View { } } + func compactPenShadow(_ pen: Pen) -> some View { + ZStack { + Group { + if let tip = pen.style.compactIcon.tip { + Image(tip) + .resizable() + .renderingMode(.template) + } + Image(pen.style.compactIcon.base) + .resizable() + .renderingMode(.template) + } + .foregroundStyle(.black.opacity(0.2)) + .blur(radius: 3) + if let tip = pen.style.compactIcon.tip { + Image(tip) + .resizable() + .renderingMode(.template) + .foregroundStyle(Color(red: pen.rgba[0], green: pen.rgba[1], blue: pen.rgba[2])) + .blur(radius: 0.5) + } + } + } + var lockButton: some View { Button { withAnimation { @@ -332,4 +552,18 @@ struct PenDock: View { .hoverEffect(.lift) .contentTransition(.symbolEffect(.replace)) } + + func createNewPen() { + let pen = PenObject.createObject(\.viewContext, penStyle: .marker) + var selectedPen = tool.selectedPen + selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last) + if let color = selectedPen?.rgba { + pen.color = color + } + pen.isSelected = true + pen.tool = tool.object + pen.orderIndex = Int16(tool.pens.count) + let _pen = Pen(object: pen) + tool.addPen(_pen) + } } diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index bb99cac..9b9531c 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -6,12 +6,11 @@ // import SwiftUI -import PhotosUI import Foundation -import AVFoundation struct Toolbar: View { @Environment(\.dismiss) var dismiss + @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject var tool: Tool @ObservedObject var canvas: Canvas @@ -19,14 +18,9 @@ struct Toolbar: View { @State var title: String @State var memo: MemoObject - @State var opensCamera: Bool = false - @State var photosPickerItem: PhotosPickerItem? - @State var isCameraAccessDenied: Bool = false @FocusState var textFieldState: Bool - @Namespace var namespace - let size: CGFloat init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) { @@ -47,8 +41,8 @@ struct Toolbar: View { } } .frame(maxWidth: .infinity, alignment: .leading) - if !canvas.locksCanvas { - elementTool + if !canvas.locksCanvas, horizontalSizeClass == .regular { + ElementToolbar(size: size, tool: tool, canvas: canvas) } HStack(spacing: 5) { if !canvas.locksCanvas { @@ -60,40 +54,6 @@ struct Toolbar: View { } .font(.subheadline) .padding(10) - .onChange(of: photosPickerItem) { oldValue, newValue in - if newValue != nil { - Task { - tool.isLoadingPhoto = true - let data = try? await newValue?.loadTransferable(type: Data.self) - if let data, let image = UIImage(data: data) { - tool.selectPhoto(image, for: canvas.canvasID) - } - photosPickerItem = nil - } - } - } - .fullScreenCover(isPresented: $opensCamera) { - let image: Binding = Binding { - tool.selectedPhotoItem?.image - } set: { image in - guard let image else { return } - tool.selectPhoto(image, for: canvas.canvasID) - } - CameraView(image: image, canvas: canvas) - .ignoresSafeArea() - } - .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { - Button { - if let url = URL(string: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { - UIApplication.shared.open(url) - } - } label: { - Text("Open Settings") - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") - } } var closeButton: some View { @@ -116,7 +76,7 @@ struct Toolbar: View { .focused($textFieldState) .textFieldStyle(.plain) .padding(.horizontal, size / 2.5) - .frame(width: 140, height: size) + .frame(width: horizontalSizeClass == .compact ? 100 : 140, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) .onChange(of: textFieldState) { oldValue, newValue in @@ -135,110 +95,6 @@ struct Toolbar: View { .transition(.move(edge: .top).combined(with: .blurReplace)) } - var elementTool: some View { - HStack(spacing: 0) { - Button { - withAnimation { - tool.selectTool(.hand) - } - } label: { - Image(systemName: "hand.draw.fill") - .fontWeight(.heavy) - .contentShape(.circle) - .frame(width: size, height: size) - .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - .background { - if tool.selection == .hand { - Color.accentColor - .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) - } - } - Button { - withAnimation { - tool.selectTool(.pen) - } - } label: { - Image(systemName: "pencil") - .fontWeight(.heavy) - .contentShape(.circle) - .frame(width: size, height: size) - .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - .background { - if tool.selection == .pen { - Color.accentColor - .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) - } - } - HStack(spacing: 0) { - Button { - withAnimation { - tool.selectTool(.photo) - } - } label: { - Image(systemName: "photo") - .contentShape(.circle) - .frame(width: size, height: size) - .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - .background { - if tool.selection == .photo { - Color.accentColor - .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) - } - if tool.selection != .photo { - Color.clear - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - } - } - if tool.selection == .photo { - HStack(spacing: 0) { - Button { - openCamera() - } label: { - Image(systemName: "camera.fill") - .contentShape(.circle) - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { - Image(systemName: "photo.fill.on.rectangle.fill") - .contentShape(.circle) - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - } - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - .transition(.blurReplace.animation(.easeIn(duration: 0.1))) - } - } - .background { - if tool.selection == .photo { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white.tertiary) - .transition(.move(edge: .leading).animation(.easeIn(duration: 0.1))) - } - } - } - .background { - RoundedRectangle(cornerRadius: 8) - .fill(.regularMaterial) - } - .transition(.move(edge: .top).combined(with: .blurReplace)) - } - var historyControl: some View { HStack { Button { @@ -288,26 +144,7 @@ struct Toolbar: View { } .hoverEffect(.lift) .contentTransition(.symbolEffect(.replace)) - } - - func openCamera() { - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { status in - withAnimation { - if status { - opensCamera = true - } else { - isCameraAccessDenied = true - } - } - } - case .authorized: - opensCamera = true - default: - isCameraAccessDenied = true - } + .transition(.move(edge: .top).combined(with: .blurReplace)) } func closeMemo() { diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/Contents.json b/Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/Contents.json new file mode 100644 index 0000000..58541c1 --- /dev/null +++ b/Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "eraser.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "eraser@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "eraser@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/eraser.png b/Memola/Resources/Assets/Assets.xcassets/graphics/eraser/eraser-compact.imageset/eraser.png new file mode 100644 index 0000000000000000000000000000000000000000..f78d259c44bbf700a76ca866041590d2a9f4b7b1 GIT binary patch literal 4287 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 literal 0 HcmV?d00001 diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/marker-base@3x.png b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-base-compact.imageset/marker-base@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..62f9f511034d9663a991e766b06590f0ea3b0a5b GIT binary patch literal 26591 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 CfkHt5 literal 0 HcmV?d00001 diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/Contents.json b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/Contents.json new file mode 100644 index 0000000..69f40d1 --- /dev/null +++ b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "marker-tip.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "marker-tip@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "marker-tip@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip.png b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip.png new file mode 100644 index 0000000000000000000000000000000000000000..1667820a9e39fc3e79749d11f21dca711bf0e52f GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^HbA_IgAGW^xvjYcq&N#aB8wRqxP?KOkzv*x37{Zj zage(c!@6@aFM%AEbVpxD28NCO+DyOv)CPc?){^4A4|TO^`?0w+iv?Zq3oE+HRT;o-&!r%D0i?t^SN2? zSB|`&+b;jv|Krm6g3sYcz2hG~I~bmsKY81%f5-00GwAg_c*PQ~zQgL^RnB#O1*J`^ ng=6O86Z*S50T_tbLBu}&`jhKkU3yZe15)7W>gTe~DWM4fKB%}P literal 0 HcmV?d00001 diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@2x.png b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f0364416d545209db16f4bbf5f20e718cf1f4c41 GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFDI9D-)@%0vZ9s~%z$3Dlfk8|agc&`9R6YO& z8H`|Qm^XiWu&kAo&ENm{PWn0r}j+U zt^91=JM(#L+e%|%u0Kxevz#S+_3WPY?Z<6puFB12GrgL5yLtxy(Fu94zU*k9*|pH8 zbl;B8HCk<-_MMZinCENQbK9!^!R#asAa&>Pmi>$Cg8s&S3SXH2`)ACWwBYHt?^WKi z7q?$`Z{6!n&vx8S5)F-n-hJSCGYQvD?dB-t$VshQ_S>g(e2y6(pJA_)889d z98=E`apBM7KY0&#YKj?N{Zx2c?y_wEi>-MTQ9uUs{&int#KleXFXkQHxjJoE?uTX9 zN=*->TX=0hkS%wfeRkFnHu?9Vg@VCrtN;G3yk(da zxt|n$C!_SngVj=>&t6^K|udS?83{1OR&&XFC7@ literal 0 HcmV?d00001 diff --git a/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@3x.png b/Memola/Resources/Assets/Assets.xcassets/graphics/pens/marker-tip-compact.imageset/marker-tip@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2b0a33ab936b125867658543f0e4eee887c30767 GIT binary patch literal 1452 zcmeAS@N?(olHy`uVBq!ia0vp^TNoIa#5mZ1tbc9G{{bn^0*}aI1_nh75N33pW|#mJ zWGoJHcVbv~PUa<$!;&U>cv7h@-A}a#}tD~ojV@L(#+q-8=WkLno9x|D|*`WMD zh%v<>y}@{cQ3KB#9u7yv89Y90Ee0tJ(P2ryZZx|)C7=BCr=)A(t z1Lli;&WjV%IkV!h^=IeL;d_6dXXyTTWyW{?uJc@C4?=6>&g`y?E=XM5C;U0DPIJeC z%21263!m+5x%3{a>XY7lwYgie^v~tW`ejYVyVhMkYIA;>&DRG$Zgb6^-QA;S(fr!V z|H5b1i<)OWd#(J>?Ebm+LCd<=dmPUjbNMA)`I$WL`K|{n&o<=gnJj+L;TEfxw%o+s zoV~R8;m0G*izU9k6TG#wxh}SVCA{?KjOQO?ehKc=lKZr5n&74Dw|}Nv?-#eZ7;@TT z?sxypicLRD;({)G_F?rm$f`6v|9P9mi_?!LtelpqHT`gN3>Sa&snid4dhIonR!-ZQ zw?oG6z^TIh)3@e));=Ec!DDvRH%{C0vzy#20#El>ZZF#*b1(7fkNxwrWS`Ag&n35^ zCEum$%^uw!9b4B(|4x2;Bl+o$T;12l_N{JWHru-H^37WZ9fQ|ur(_?uT_YpEXT{g3 z&-dpF-@LNR@9E#4YZK0|C}piF-*fWQys3}puDq7pcT-T?eD3{lkM;7peMdjP{S(VuYPWE5{H)vSWal=| z)0)0O`q#c9)%sldtClYUcP`6X^?IqnPp2K1KPBDx+VXBga$nYp>%C1%zo%!_}$_^{{iv_8l3s>2T}O_QIVxc2U@lViH{)t&{Nk2b&Uxb#sy zEN|8Hu>Z&WG8S>n(r8laTF|L9%A{^MfC{A%09Mp9=4LyWMChMt0QuI_)z4*}Q$iB} DkG(;y literal 0 HcmV?d00001 From dbe1c22c96ca43ede39532b3c1b39d5124e56326 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Tue, 2 Jul 2024 03:06:36 +0700 Subject: [PATCH 15/16] feat: fine tune element toolbar --- .../Memo/ElementToolbar/ElementToolbar.swift | 10 ++++--- Memola/Features/Memo/Memo/MemoView.swift | 28 +++++++++++++++---- Memola/Features/Memo/PenDock/PenDock.swift | 21 ++------------ 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index 7db9ff4..9d9047d 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -26,18 +26,20 @@ struct ElementToolbar: View { if horizontalSizeClass == .regular { regularToolbar } else { - ZStack(alignment: .bottomLeading) { - compactToolbar + ZStack(alignment: .bottom) { if tool.selection == .photo { photoOption .background { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } - .frame(maxWidth: .infinity) + .padding(.bottom, 10) .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } else { + compactToolbar } } + .padding(.bottom, 10) } } .fullScreenCover(isPresented: $opensCamera) { @@ -193,8 +195,8 @@ struct ElementToolbar: View { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } - .transition(.move(edge: .bottom).combined(with: .blurReplace)) .padding(10) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) } var photoOption: some View { diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 5fd21d8..47dbb68 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -66,11 +66,11 @@ struct MemoView: View { switch tool.selection { case .pen: PenDock(tool: tool, canvas: canvas, size: size) - .transition(.move(edge: .trailing)) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) case .photo: if let photoItem = tool.selectedPhotoItem { PhotoPreview(photoItem: photoItem, tool: tool) - .transition(.move(edge: .trailing)) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } default: EmptyView() @@ -88,7 +88,7 @@ struct MemoView: View { switch tool.selection { case .pen: PenDock(tool: tool, canvas: canvas, size: size) - .transition(.move(edge: .bottom)) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) case .photo: if let photoItem = tool.selectedPhotoItem { PhotoPreview(photoItem: photoItem, tool: tool) @@ -99,9 +99,27 @@ struct MemoView: View { } } .overlay(alignment: .bottom) { - if tool.selection == .hand { + if tool.selection != .pen { ElementToolbar(size: size, tool: tool, canvas: canvas) - .transition(.move(edge: .bottom)) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + } + .overlay(alignment: .bottom) { + if tool.selection != .hand { + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "chevron.compact.down") + .font(.headline) + .frame(width: 80) + .padding(5) + .background(.regularMaterial) + .clipShape(.capsule) + .contentShape(.capsule) + } + .offset(y: 5) } } } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index 52ddbee..8624221 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -61,32 +61,17 @@ struct PenDock: View { .fill(.regularMaterial) .frame(height: height * factor - 18) } - .padding([.horizontal, .bottom], 10) + .padding(.horizontal, 10) + .padding(.bottom, 20) .frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom) .frame(maxWidth: .infinity) } - .overlay(alignment: .bottom) { - Button { - withAnimation { - tool.selectTool(.hand) - } - } label: { - Image(systemName: "chevron.compact.down") - .font(.headline) - .frame(width: 80) - .padding(10) - .background(.regularMaterial) - .clipShape(.capsule) - .contentShape(.capsule) - } - .offset(y: 5) - } .transition(.move(edge: .bottom).combined(with: .blurReplace)) } lockButton .frame(maxWidth: .infinity, alignment: .bottomTrailing) .padding(10) - .offset(y: canvas.locksCanvas ? 0 : -(height * factor - size + 20)) + .offset(y: canvas.locksCanvas ? 0 : -(height * factor - size + 30)) .transition(.move(edge: .trailing).combined(with: .blurReplace)) } } From 23fa2dbf90c19797941fcd719afaa68a86fadb40 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Wed, 3 Jul 2024 00:32:41 +0700 Subject: [PATCH 16/16] feat: fine tune photo preview --- Memola/Features/Memo/ElementToolbar/ElementToolbar.swift | 2 ++ Memola/Features/Memo/Memo/MemoView.swift | 1 + Memola/Features/Memo/PhotoPreview/PhotoPreview.swift | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index 9d9047d..d3c62a3 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -34,6 +34,7 @@ struct ElementToolbar: View { .fill(.regularMaterial) } .padding(.bottom, 10) + .frame(maxWidth: .infinity) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } else { compactToolbar @@ -196,6 +197,7 @@ struct ElementToolbar: View { .fill(.regularMaterial) } .padding(10) + .frame(maxWidth: .infinity) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 47dbb68..722e431 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -92,6 +92,7 @@ struct MemoView: View { case .photo: if let photoItem = tool.selectedPhotoItem { PhotoPreview(photoItem: photoItem, tool: tool) + .frame(maxWidth: .infinity, alignment: .trailing) .transition(.move(edge: .trailing)) } default: diff --git a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift index 16e7399..beb923e 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift @@ -8,6 +8,8 @@ import SwiftUI struct PhotoPreview: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + let photoItem: PhotoItem @ObservedObject var tool: Tool @@ -15,7 +17,7 @@ struct PhotoPreview: View { Image(uiImage: photoItem.previewImage) .resizable() .scaledToFit() - .frame(height: 100) + .frame(width: horizontalSizeClass == .compact ? 80 : nil, height: horizontalSizeClass == .compact ? nil : 100) .cornerRadius(5) .overlay { RoundedRectangle(cornerRadius: 5)