259 lines
8.2 KiB
Swift
259 lines
8.2 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
@Observable
|
|
@MainActor
|
|
final class WorkspaceStore {
|
|
var notebooks: [Notebook] = []
|
|
var memos: [MemoSummary] = []
|
|
var selectedNotebookId: String?
|
|
var searchText = ""
|
|
var sort: MemoSortMode = .updatedDesc
|
|
var filter: MobileMemoFilterMode = .all
|
|
var selectedTag: String?
|
|
var totalCount = 0
|
|
var nextOffset: Int?
|
|
var isLoadingList = false
|
|
var isLoadingMore = false
|
|
var listError: String?
|
|
var selectionMode = false
|
|
var selectedMemoIds: Set<String> = []
|
|
var showNotebookPicker = false
|
|
var startCreatingNotebook = false
|
|
var showActions = false
|
|
var showTagFilterPicker = false
|
|
|
|
private var searchTask: Task<Void, Never>?
|
|
|
|
var activeNotebook: Notebook? {
|
|
guard let selectedNotebookId else { return nil }
|
|
return notebooks.first { $0.id == selectedNotebookId }
|
|
}
|
|
|
|
func reload(env: AppEnvironment, resetOffset: Bool = true) {
|
|
guard let scope = env.session.dataScope else {
|
|
notebooks = []
|
|
memos = []
|
|
return
|
|
}
|
|
do {
|
|
notebooks = try env.mirror.listNotebooks(scope: scope)
|
|
try loadMemos(env: env, scope: scope, resetOffset: resetOffset)
|
|
listError = nil
|
|
} catch {
|
|
listError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func scheduleSearch(env: AppEnvironment) {
|
|
searchTask?.cancel()
|
|
searchTask = Task {
|
|
try? await Task.sleep(nanoseconds: 250_000_000)
|
|
guard !Task.isCancelled else { return }
|
|
reload(env: env)
|
|
}
|
|
}
|
|
|
|
func loadMemos(env: AppEnvironment, scope: String, resetOffset: Bool) throws {
|
|
isLoadingList = resetOffset
|
|
defer { isLoadingList = false }
|
|
let offset = resetOffset ? 0 : (nextOffset ?? memos.count)
|
|
let notebookIds: [String]? = {
|
|
guard selectedTag == nil, let selectedNotebookId else { return nil }
|
|
return NotebookHierarchy.scopeIds(
|
|
notebooks: notebooks,
|
|
targetNotebookId: selectedNotebookId,
|
|
includeDescendants: env.preferences.showDescendantNotes
|
|
)
|
|
}()
|
|
let result = try env.mirror.listMemos(
|
|
scope: scope,
|
|
params: LocalMemoListParams(
|
|
notebookId: nil,
|
|
notebookIds: notebookIds,
|
|
q: searchText,
|
|
tag: selectedTag,
|
|
sort: sort,
|
|
filter: filter.toMemoFilterMode(),
|
|
limit: 50,
|
|
offset: offset
|
|
)
|
|
)
|
|
if resetOffset {
|
|
memos = result.memos
|
|
} else {
|
|
let existing = Set(memos.map(\.id))
|
|
memos.append(contentsOf: result.memos.filter { !existing.contains($0.id) })
|
|
}
|
|
totalCount = result.totalCount
|
|
nextOffset = result.nextOffset
|
|
}
|
|
|
|
func loadMore(env: AppEnvironment) {
|
|
guard let scope = env.session.dataScope, nextOffset != nil, !isLoadingMore else { return }
|
|
isLoadingMore = true
|
|
defer { isLoadingMore = false }
|
|
try? loadMemos(env: env, scope: scope, resetOffset: false)
|
|
}
|
|
|
|
func toggleFilter(_ requested: MobileMemoFilterMode) {
|
|
selectedTag = nil
|
|
filter = MobileUI.toggleMemoFilterMode(current: filter, requested: requested)
|
|
}
|
|
|
|
func selectTag(_ tag: String?) {
|
|
selectedTag = tag
|
|
selectedNotebookId = nil
|
|
filter = .all
|
|
searchText = ""
|
|
clearSelection()
|
|
}
|
|
|
|
func clearTagFilter() {
|
|
selectedTag = nil
|
|
filter = .all
|
|
clearSelection()
|
|
}
|
|
|
|
func enterSelection(memoId: String? = nil) {
|
|
selectionMode = true
|
|
if let memoId {
|
|
selectedMemoIds = [memoId]
|
|
}
|
|
}
|
|
|
|
func clearSelection() {
|
|
selectionMode = false
|
|
selectedMemoIds = []
|
|
}
|
|
|
|
func toggleSelected(_ memoId: String) {
|
|
selectedMemoIds = MobileUI.toggleMemoSelection(current: selectedMemoIds, memoId: memoId)
|
|
}
|
|
|
|
var allVisibleMemosSelected: Bool {
|
|
!memos.isEmpty && memos.allSatisfy { selectedMemoIds.contains($0.id) }
|
|
}
|
|
|
|
var nextSelectionPinValue: Bool {
|
|
let selected = memos.filter { selectedMemoIds.contains($0.id) }
|
|
return !selected.isEmpty && !selected.allSatisfy(\.isPinned)
|
|
}
|
|
|
|
func toggleVisibleSelection() {
|
|
let visibleIds = Set(memos.map(\.id))
|
|
if allVisibleMemosSelected {
|
|
selectedMemoIds.subtract(visibleIds)
|
|
} else {
|
|
selectionMode = true
|
|
selectedMemoIds.formUnion(visibleIds)
|
|
}
|
|
}
|
|
|
|
func softDelete(env: AppEnvironment, memoId: String) async {
|
|
guard let scope = env.session.dataScope else { return }
|
|
do {
|
|
if memoId.hasPrefix("local:") {
|
|
try env.outbox.cancelMemo(scope: scope, memoId: memoId)
|
|
try env.mirror.deleteMemo(scope: scope, id: memoId)
|
|
} else {
|
|
_ = try env.mirror.softDeleteMemo(scope: scope, id: memoId)
|
|
try await env.session.client.deleteMemo(id: memoId, permanent: false)
|
|
}
|
|
selectedMemoIds.remove(memoId)
|
|
reload(env: env)
|
|
} catch {
|
|
listError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func softDeleteSelection(env: AppEnvironment) async {
|
|
let ids = Array(selectedMemoIds)
|
|
for id in ids {
|
|
await softDelete(env: env, memoId: id)
|
|
}
|
|
clearSelection()
|
|
}
|
|
|
|
func togglePin(env: AppEnvironment, memo: MemoSummary) async {
|
|
guard let scope = env.session.dataScope else { return }
|
|
do {
|
|
let updated = try await env.session.client.updateMemo(
|
|
id: memo.id,
|
|
expectedRevision: nil,
|
|
expectedContentHash: nil,
|
|
editSessionId: nil,
|
|
notebookId: nil,
|
|
title: nil,
|
|
isPinned: !memo.isPinned,
|
|
contentMarkdown: nil,
|
|
tags: nil
|
|
)
|
|
try env.mirror.upsertMemo(scope: scope, memo: updated)
|
|
reload(env: env)
|
|
} catch {
|
|
listError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func pinSelection(env: AppEnvironment, isPinned: Bool) async {
|
|
guard let scope = env.session.dataScope, !selectedMemoIds.isEmpty else { return }
|
|
do {
|
|
for memoId in selectedMemoIds {
|
|
let updated = try await env.session.client.updateMemo(
|
|
id: memoId,
|
|
expectedRevision: nil,
|
|
expectedContentHash: nil,
|
|
editSessionId: nil,
|
|
notebookId: nil,
|
|
title: nil,
|
|
isPinned: isPinned,
|
|
contentMarkdown: nil,
|
|
tags: nil
|
|
)
|
|
try env.mirror.upsertMemo(scope: scope, memo: updated)
|
|
}
|
|
clearSelection()
|
|
reload(env: env)
|
|
} catch {
|
|
listError = error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func moveSelection(env: AppEnvironment, notebookId: String) async {
|
|
guard let scope = env.session.dataScope else { return }
|
|
for memoId in selectedMemoIds {
|
|
guard var memo = try? env.mirror.resolveMemo(scope: scope, id: memoId) else { continue }
|
|
memo.notebookId = notebookId
|
|
memo.updatedAt = EdgeEverDate.nowString()
|
|
try? env.mirror.upsertMemo(scope: scope, memo: memo)
|
|
try? env.outbox.enqueueUpdate(
|
|
scope: scope,
|
|
payload: MemoUpdatePayload(
|
|
memoId: memo.id,
|
|
expectedRevision: memo.revision,
|
|
expectedContentHash: memo.contentHash,
|
|
title: memo.title ?? "",
|
|
contentMarkdown: memo.contentMarkdown,
|
|
notebookId: notebookId,
|
|
tags: memo.tags
|
|
)
|
|
)
|
|
}
|
|
await env.runSyncCycle()
|
|
clearSelection()
|
|
reload(env: env)
|
|
}
|
|
}
|
|
|
|
extension MobileMemoFilterMode {
|
|
func toMemoFilterMode() -> MemoFilterMode {
|
|
switch self {
|
|
case .all: return .all
|
|
case .tagged: return .tagged
|
|
case .untagged: return .untagged
|
|
case .pinned: return .pinned
|
|
}
|
|
}
|
|
}
|