mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-20 00:24:12 +01:00
feat: implement trash view
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
EC01511F2C305D7B008A115E /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = "<group>"; };
|
||||
EC0151222C306089008A115E /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||
EC0151252C3067B9008A115E /* TrashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashView.swift; sourceTree = "<group>"; };
|
||||
EC0151292C306935008A115E /* MemoGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoGrid.swift; sourceTree = "<group>"; };
|
||||
EC01512B2C306BEF008A115E /* MemoCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoCard.swift; sourceTree = "<group>"; };
|
||||
EC01512D2C30727F008A115E /* MemoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoPreview.swift; sourceTree = "<group>"; };
|
||||
EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = "<group>"; };
|
||||
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
|
||||
@@ -239,6 +253,61 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
EC01511A2C305ABB008A115E /* Dashboard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC0151272C306906008A115E /* Details */,
|
||||
EC0151212C30605F008A115E /* Sidebar */,
|
||||
EC01511C2C305C99008A115E /* Dashboard */,
|
||||
);
|
||||
path = Dashboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC01511C2C305C99008A115E /* Dashboard */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC01511D2C305CA9008A115E /* DashboardView.swift */,
|
||||
);
|
||||
path = Dashboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC0151212C30605F008A115E /* Sidebar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC0151222C306089008A115E /* Sidebar.swift */,
|
||||
EC01511F2C305D7B008A115E /* SidebarItem.swift */,
|
||||
);
|
||||
path = Sidebar;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC0151242C3067B2008A115E /* Trash */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC0151252C3067B9008A115E /* TrashView.swift */,
|
||||
);
|
||||
path = Trash;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC0151272C306906008A115E /* Details */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC0151282C306927008A115E /* Shared */,
|
||||
EC0151242C3067B2008A115E /* Trash */,
|
||||
ECA738782BE5EEF700A4542E /* Memos */,
|
||||
);
|
||||
path = Details;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC0151282C306927008A115E /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC0151292C306935008A115E /* MemoGrid.swift */,
|
||||
EC01512B2C306BEF008A115E /* MemoCard.swift */,
|
||||
EC01512D2C30727F008A115E /* MemoPreview.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC1437B42BE748E60022C903 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -418,8 +487,8 @@
|
||||
ECA738772BE5EEE800A4542E /* Features */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC01511A2C305ABB008A115E /* Dashboard */,
|
||||
ECA7387B2BE5EF3500A4542E /* Memo */,
|
||||
ECA738782BE5EEF700A4542E /* Memos */,
|
||||
);
|
||||
path = Features;
|
||||
sourceTree = "<group>";
|
||||
@@ -427,9 +496,9 @@
|
||||
ECA738782BE5EEF700A4542E /* Memos */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECA738792BE5EF0400A4542E /* MemosView.swift */,
|
||||
EC1815072C2D980B00541369 /* Sort.swift */,
|
||||
EC1815092C2DA09E00541369 /* Filter.swift */,
|
||||
ECA738792BE5EF0400A4542E /* MemosView.swift */,
|
||||
);
|
||||
path = Memos;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
29
Memola/Features/Dashboard/Dashboard/DashboardView.swift
Normal file
29
Memola/Features/Dashboard/Dashboard/DashboardView.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<MemoObject>] = []
|
||||
@@ -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()
|
||||
}
|
||||
@@ -46,4 +46,19 @@ extension Sort {
|
||||
return [SortDescriptor(\.createdAt)]
|
||||
}
|
||||
}
|
||||
|
||||
var trashSortDescriptors: [SortDescriptor<MemoObject>] {
|
||||
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)]
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Memola/Features/Dashboard/Details/Shared/MemoCard.swift
Normal file
37
Memola/Features/Dashboard/Details/Shared/MemoCard.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// MemoCard.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 6/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MemoCard<Preview: View, Detail: View>: 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Memola/Features/Dashboard/Details/Shared/MemoGrid.swift
Normal file
36
Memola/Features/Dashboard/Details/Shared/MemoGrid.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// MemoGrid.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 6/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MemoGrid<Card: View>: View {
|
||||
let cellWidth: CGFloat = 250
|
||||
let cellHeight: CGFloat = 150
|
||||
|
||||
let memoObjects: FetchedResults<MemoObject>
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Memola/Features/Dashboard/Details/Shared/MemoPreview.swift
Normal file
18
Memola/Features/Dashboard/Details/Shared/MemoPreview.swift
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
85
Memola/Features/Dashboard/Details/Trash/TrashView.swift
Normal file
85
Memola/Features/Dashboard/Details/Trash/TrashView.swift
Normal file
@@ -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<MemoObject>
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
78
Memola/Features/Dashboard/Sidebar/Sidebar.swift
Normal file
78
Memola/Features/Dashboard/Sidebar/Sidebar.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
35
Memola/Features/Dashboard/Sidebar/SidebarItem.swift
Normal file
35
Memola/Features/Dashboard/Sidebar/SidebarItem.swift
Normal file
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</entity>
|
||||
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isFavorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="isTrash" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
|
||||
Reference in New Issue
Block a user