mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-06 15:21:21 +08:00

This way the disk barely needs to be accessed when loading GitJournal, and we can hide how long it takes GitJournal to parse all the notes in the background.
169 lines
4.3 KiB
Dart
169 lines
4.3 KiB
Dart
/*
|
|
* SPDX-FileCopyrightText: 2019-2021 Vishesh Handa <me@vhanda.in>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:universal_io/io.dart' as io;
|
|
|
|
import 'package:gitjournal/core/file/file_storage.dart';
|
|
import 'package:gitjournal/core/folder/notes_folder_fs.dart';
|
|
import 'package:gitjournal/core/folder/sorting_mode.dart';
|
|
import 'package:gitjournal/core/note.dart';
|
|
import 'package:gitjournal/error_reporting.dart';
|
|
import 'package:gitjournal/generated/core.pb.dart' as pb;
|
|
import 'package:gitjournal/logger/logger.dart';
|
|
|
|
class NotesCache {
|
|
final String folderPath;
|
|
final String repoPath;
|
|
final FileStorage fileStorage;
|
|
|
|
static const enabled = true;
|
|
|
|
String get filePath => p.join(folderPath, 'notes_cache_v$version');
|
|
|
|
static const CACHE_SIZE = 20;
|
|
static const version = 1;
|
|
|
|
NotesCache({
|
|
required this.folderPath,
|
|
required this.repoPath,
|
|
required this.fileStorage,
|
|
}) {
|
|
assert(repoPath.endsWith(p.separator));
|
|
assert(folderPath.startsWith(p.separator));
|
|
}
|
|
|
|
Future<void> load(NotesFolderFS rootFolder) async {
|
|
if (!enabled) return;
|
|
|
|
var pbNotes = await loadFromDisk();
|
|
Log.i("Notes Cache Loaded: ${pbNotes.length} items");
|
|
|
|
assert(repoPath.endsWith(p.separator));
|
|
|
|
var selectFolders = <NotesFolderFS>{};
|
|
for (var pbNote in pbNotes) {
|
|
var parentFolderPath = p.dirname(pbNote.file.filePath);
|
|
var parent = rootFolder.getOrBuildFolderWithSpec(parentFolderPath);
|
|
|
|
var note = Note.fromProtoBuf(parent, pbNote);
|
|
parent.addFile(note);
|
|
var _ = selectFolders.add(parent);
|
|
}
|
|
|
|
// FIXME: Avoid having to call loadNotes,
|
|
// we can cache everything required from these notes
|
|
// Maybe use protobuf?
|
|
|
|
// Load all the notes recursively
|
|
// var futures = selectFolders.map((f) => f.loadNotes()).toList();
|
|
// var _ = await Future.wait(futures);
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
if (!enabled) return;
|
|
var _ = await io.File(filePath).delete();
|
|
}
|
|
|
|
Future<void> buildCache(NotesFolderFS rootFolder) async {
|
|
if (!enabled) return;
|
|
|
|
var notes = rootFolder.getAllNotes();
|
|
var sortingMode = rootFolder.config.sortingMode;
|
|
|
|
var topNotes = _fetchTop(notes, sortingMode);
|
|
var pinned = _fetchPinned(notes);
|
|
var fileList = pinned.followedBy(topNotes);
|
|
|
|
return saveToDisk(fileList);
|
|
}
|
|
|
|
Iterable<Note> _fetchTop(
|
|
Iterable<Note> allNotes,
|
|
SortingMode sortingMode,
|
|
) {
|
|
var origFn = sortingMode.sortingFunction();
|
|
|
|
reversedFn(Note a, Note b) {
|
|
var r = origFn(a, b);
|
|
if (r < 0) return 1;
|
|
if (r > 0) return -1;
|
|
return 0;
|
|
}
|
|
|
|
var heap = HeapPriorityQueue<Note>(reversedFn);
|
|
|
|
for (var note in allNotes) {
|
|
if (!heap.contains(note)) {
|
|
heap.add(note);
|
|
}
|
|
if (heap.length > CACHE_SIZE) {
|
|
var _ = heap.removeFirst();
|
|
}
|
|
}
|
|
|
|
return heap.toList().reversed;
|
|
}
|
|
|
|
Iterable<Note> _fetchPinned(Iterable<Note> allNotes) sync* {
|
|
for (var note in allNotes) {
|
|
if (note.pinned) {
|
|
yield note;
|
|
}
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<List<pb.Note>> loadFromDisk() async {
|
|
var contents = Uint8List(0);
|
|
try {
|
|
assert(filePath.startsWith(p.separator));
|
|
contents = await io.File(filePath).readAsBytes();
|
|
} on io.FileSystemException catch (ex) {
|
|
if (ex.osError?.errorCode == 2 /* file not found */) {
|
|
return [];
|
|
}
|
|
rethrow;
|
|
}
|
|
|
|
if (contents.isEmpty) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
return pb.NoteList.fromBuffer(contents).notes;
|
|
} catch (ex, st) {
|
|
Log.e("Failed to load NotesCache", ex: ex, stacktrace: st);
|
|
await logExceptionWarning(ex, st);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<void> saveToDisk(Iterable<Note> notes) async {
|
|
var contents = pb.NoteList(
|
|
notes: notes.map((n) => n.toProtoBuf() as pb.Note),
|
|
).writeToBuffer();
|
|
var newFilePath = filePath + ".new";
|
|
|
|
try {
|
|
assert(newFilePath.startsWith(p.separator));
|
|
var file = io.File(newFilePath);
|
|
dynamic _;
|
|
_ = await file.writeAsBytes(contents);
|
|
_ = await file.rename(filePath);
|
|
} catch (ex, st) {
|
|
// FIXME: Do something in this case!!
|
|
Log.e("Failed to save Notes Cache", ex: ex, stacktrace: st);
|
|
}
|
|
}
|
|
}
|