From 63a619edf913faff9968c0c9d32c48c71c7b45fd Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 30 Jun 2024 00:31:56 +0700 Subject: [PATCH] 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 @@ +