Skip to content
17 changes: 14 additions & 3 deletions lib/components/canvas/save_indicator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,26 @@ class SaveIndicator extends StatelessWidget {
icon: switch (savingState.value) {
SavingState.waitingToSave => const Icon(Icons.save),
SavingState.saving => const CircularProgressIndicator.adaptive(),
SavingState.saved => const Icon(Icons.arrow_back),
SavingState.savedWithoutThumbnail => const Icon(Icons.arrow_back),
SavingState.savedWithThumbnail => const Icon(Icons.arrow_back),
},
),
);
},
);
}

/// When the save/exit button is pressed
void _onPressed(BuildContext context) {
switch (savingState.value) {
case SavingState.waitingToSave:
triggerSave();
case SavingState.saving:
break;
case SavingState.saved:
case SavingState.savedWithoutThumbnail:
triggerSave(); // triggering save will be created thumbnail and then is finished editing
_back(context);
case SavingState.savedWithThumbnail:
_back(context);
}
}
Expand All @@ -62,5 +67,11 @@ class SaveIndicator extends StatelessWidget {
enum SavingState {
waitingToSave,
saving,
saved,

/// Saved, but the thumbnail still needs updating.
/// (Thumbnails aren't created when auto-saving to avoid lag.)
savedWithoutThumbnail,

/// Saved, and the thumbnail is up-to-date.
savedWithThumbnail,
}
3 changes: 2 additions & 1 deletion lib/components/home/preview_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class _PreviewCardState extends State<PreviewCard> {

StreamSubscription? fileWriteSubscription;
void fileWriteListener(FileOperation event) {
if (event.filePath != widget.filePath) return;
/// listen to changes of note preview (thumbnail) file to see if it needs to be updated
if (event.filePath != '${widget.filePath}${Editor.extension}.p') return;
if (event.type == FileOperationType.delete) {
thumbnail.image = null;
} else if (event.type == FileOperationType.write) {
Expand Down
3 changes: 2 additions & 1 deletion lib/components/navbar/responsive_navbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class _ResponsiveNavbarState extends State<ResponsiveNavbar> {
final savingState = Whiteboard.savingState;
switch (savingState) {
case null:
case SavingState.saved:
case SavingState.savedWithoutThumbnail:
case SavingState.savedWithThumbnail:
break;
case SavingState.waitingToSave:
Whiteboard.triggerSave();
Expand Down
45 changes: 34 additions & 11 deletions lib/pages/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class EditorState extends State<Editor> {
Prefs.lastTool.value = tool.toolId;
}

ValueNotifier<SavingState> savingState = ValueNotifier(SavingState.saved);
final savingState = ValueNotifier(SavingState.savedWithThumbnail);
Timer? _delayedSaveTimer;

// used to prevent accidentally drawing when pinch zooming
Expand Down Expand Up @@ -258,7 +258,8 @@ class EditorState extends State<Editor> {
clearAllPages();

// save cleared whiteboard
await saveToFile();
// without thumbanil as whiteboard thumbnail is never used
await saveToFile(createThumbnail: false);
Whiteboard.needsToAutoClearWhiteboard = false;
} else {
setState(() {});
Expand Down Expand Up @@ -806,17 +807,23 @@ class EditorState extends State<Editor> {
if (Prefs.autosaveDelay.value < 0) return;
_delayedSaveTimer =
Timer(Duration(milliseconds: Prefs.autosaveDelay.value), () {
saveToFile();
saveToFile(createThumbnail: false);
});
}

Future<void> saveToFile() async {
Future<void> saveToFile({required bool createThumbnail}) async {
// createThumbnail=false is used when called from autosave - to avoid lagging during thumbnail creation
if (coreInfo.readOnly) return;

switch (savingState.value) {
case SavingState.saved:
case SavingState.savedWithThumbnail:
// avoid saving if nothing has changed
return;
case SavingState.savedWithoutThumbnail:
// note is saved, but thumbnail need to be created
createThumbnailPreview();
savingState.value = SavingState.savedWithThumbnail;
return;
case SavingState.saving:
// avoid saving if already saving
log.warning('saveToFile() called while already saving');
Expand Down Expand Up @@ -852,14 +859,29 @@ class EditorState extends State<Editor> {
numAssets: assets.length,
),
]);
savingState.value = SavingState.saved;
if (createThumbnail) {
savingState.value = SavingState.savedWithThumbnail;
} else {
savingState.value = SavingState.savedWithoutThumbnail;
}
} catch (e) {
log.severe('Failed to save file: $e', e);
savingState.value = SavingState.waitingToSave;
if (kDebugMode) rethrow;
return;
}

if (!mounted) return;
if (createThumbnail) {
createThumbnailPreview();
}
}

/// create thumbnail of note
Future<void> createThumbnailPreview() async {
if (coreInfo.readOnly) return;
final filePath = coreInfo.filePath + Editor.extension;

if (!mounted) return;
final screenshotter = ScreenshotController();
final page = coreInfo.pages.first;
Expand Down Expand Up @@ -1553,17 +1575,18 @@ class EditorState extends State<Editor> {
builder: (context, savingState, child) {
// don't allow user to go back until saving is done
return PopScope(
canPop: savingState == SavingState.saved,
canPop: savingState == SavingState.savedWithThumbnail,
onPopInvoked: (didPop) {
switch (savingState) {
case SavingState.waitingToSave:
assert(!didPop);
saveToFile(); // trigger save now
saveToFile(createThumbnail: true); // trigger save now
snackBarNeedsToSaveBeforeExiting();
case SavingState.saving:
assert(!didPop);
snackBarNeedsToSaveBeforeExiting();
case SavingState.saved:
case SavingState.savedWithoutThumbnail:
case SavingState.savedWithThumbnail:
break;
}
},
Expand Down Expand Up @@ -1592,7 +1615,7 @@ class EditorState extends State<Editor> {
),
leading: SaveIndicator(
savingState: savingState,
triggerSave: saveToFile,
triggerSave: () => saveToFile(createThumbnail: true),
),
actions: [
IconButton(
Expand Down Expand Up @@ -1958,7 +1981,7 @@ class EditorState extends State<Editor> {
await _renameFileNow();
filenameTextEditingController.dispose();
}
await saveToFile();
await saveToFile(createThumbnail: true);
})();

DynamicMaterialApp.removeFullscreenListener(_setState);
Expand Down
2 changes: 1 addition & 1 deletion lib/pages/home/whiteboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Whiteboard extends StatelessWidget {
final editorState = _whiteboardKey.currentState;
if (editorState == null) return;
assert(editorState.savingState.value == SavingState.waitingToSave);
editorState.saveToFile();
editorState.saveToFile(createThumbnail: false);
editorState.snackBarNeedsToSaveBeforeExiting();
}

Expand Down
2 changes: 1 addition & 1 deletion test/editor_undo_redo_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ void main() {
// save file now to supersede the save timer (which would run after the test is finished)
printOnFailure('Saving file: $filePath${Editor.extension}');
await tester.runAsync(() async {
await editorState.saveToFile();
await editorState.saveToFile(createThumbnail: false);
await Future.delayed(const Duration(milliseconds: 100));
await FileManager.deleteFile(filePath + Editor.extension);
});
Expand Down