Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/common/rest_api/file_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ class NoteFileHelper with PodOperationsMixin {
bool overwrite = false,
bool isExternal = false,
}) async {
// Tracks whether the `Saving the note!` animation is still on screen
// so we can guarantee it is dismissed exactly once on any code path.

var loadingDialogShown = true;

try {
// Encrypt note text using created time as the key
// av: 20250519 - We need to encrypt the note text because
Expand Down Expand Up @@ -340,16 +345,49 @@ class NoteFileHelper with PodOperationsMixin {

Navigator.of(context, rootNavigator: true)
.pop(); // Dismiss the saving note dialog
loadingDialogShown = false;

scaffoldController.navigateToSubpage(childPage);

if (!context.mounted) {
throw Exception('Context not found');
}
} on NotLoggedInException catch (e) {
debugPrint(
'NotLoggedInException (encrypting and saving note):\n $e',
);

// Dismiss the in-flight `Saving the note!` animation so the UI
// does not appear to hang while we prompt the user to log in.

if (loadingDialogShown && context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
loadingDialogShown = false;
}

if (!context.mounted) return;

// Prompt the user to log in (or cancel back to the note editor).
// Using solidui's shared `SolidLoginRequiredDialog` keeps the
// look-and-feel and the redirect-to-Solid-login-page behaviour
// consistent with the rest of the app.

await SolidLoginRequiredDialog.showAndHandle(
context,
message: 'Please log in to your POD first before saving the note.',
);
} on Exception catch (e) {
debugPrint(
'Exception (encrypting and saving note, and navigating to return page):\n $e',
);

// Make sure the loading dialog is always dismissed on failure so
// the UI never gets stuck on `Saving the note!`.

if (loadingDialogShown && context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
loadingDialogShown = false;
}
}
}
}
45 changes: 43 additions & 2 deletions lib/notes/list_my_notes_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ library;

import 'package:flutter/material.dart';

import 'package:solidpod/solidpod.dart' show isUserLoggedIn;
import 'package:solidui/solidui.dart';

import 'package:notepod/constants/app.dart';
Expand All @@ -34,6 +35,7 @@ import 'package:notepod/notes/new_note.dart';
import 'package:notepod/services/note_service.dart';
import 'package:notepod/widgets/err_card.dart';
import 'package:notepod/widgets/msg_card.dart';
import 'package:notepod/widgets/not_logged_in_card.dart';
import 'package:notepod/widgets/note_list_del_dialog.dart';

/// A [StatefulWidget] that fetches the user's notes in their app data folder
Expand All @@ -59,7 +61,12 @@ class _ListMyNotesScreenState extends State<ListMyNotesScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

/// Future function to retrieve user's notes list
static Future? _asyncDataFetch;
Future<NotesCallResult>? _asyncDataFetch;

/// Tracks the user's login status. `null` while the asynchronous
/// check is in flight (used to show the loading screen), then
/// updated by [_checkLoginAndFetch]
bool? _isLoggedIn;

/// Scroll controller for single child scroll view
late final ScrollController _scrollController;
Expand All @@ -71,8 +78,24 @@ class _ListMyNotesScreenState extends State<ListMyNotesScreen> {
void initState() {
super.initState();
_scaffoldController = widget.scaffoldController;
_asyncDataFetch = NoteService().getOwnNoteList();
_scrollController = ScrollController();
_checkLoginAndFetch();
}

/// Confirms the user is logged in before triggering the POD fetch.
///
/// When the user is not logged in we skip the fetch entirely and let
/// [build] render the [NotLoggedInCard] placeholder. Touching the POD
/// APIs without an authenticated session would otherwise produce a
/// cascade of rendering exceptions on this screen.

Future<void> _checkLoginAndFetch() async {
final loggedIn = await isUserLoggedIn();
if (!mounted) return;
setState(() {
_isLoggedIn = loggedIn;
_asyncDataFetch = loggedIn ? NoteService().getOwnNoteList() : null;
});
}

@override
Expand Down Expand Up @@ -151,6 +174,24 @@ class _ListMyNotesScreenState extends State<ListMyNotesScreen> {

@override
Widget build(BuildContext context) {
// Show the loading screen until the asynchronous login check has
// returned. Once we know the user is logged out, render the
// friendly `Not logged in` placeholder rather than attempting to
// fetch notes from a POD we cannot read.

if (_isLoggedIn == null) {
return Scaffold(
key: _scaffoldKey,
body: SafeArea(child: loadingScreen(normalLoadingScreenHeight)),
);
}
if (_isLoggedIn == false) {
return Scaffold(
key: _scaffoldKey,
body: const SafeArea(child: NotLoggedInCard()),
);
}

return Scaffold(
key: _scaffoldKey,
body: SafeArea(
Expand Down
70 changes: 58 additions & 12 deletions lib/notes/list_notes_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ library;

import 'package:flutter/material.dart';

import 'package:solidpod/solidpod.dart' show isUserLoggedIn;
import 'package:solidui/solidui.dart';

import 'package:notepod/common/rest_api/rest_api.dart';
Expand All @@ -35,6 +36,7 @@ import 'package:notepod/notes/new_note.dart';
import 'package:notepod/services/note_service.dart';
import 'package:notepod/widgets/err_card.dart';
import 'package:notepod/widgets/msg_card.dart';
import 'package:notepod/widgets/not_logged_in_card.dart';
import 'package:notepod/widgets/note_list_del_dialog.dart';
import 'package:notepod/widgets/note_list_revoke_dialog.dart';

Expand All @@ -60,13 +62,19 @@ class ListNotesScreen extends StatefulWidget {
class _ListNotesScreenState extends State<ListNotesScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

/// Future function to retrieve user's notes list
// static Future? _fetchOwnNotes;
late Future<NotesCallResult> _fetchOwnNotes;
/// Future function to retrieve user's notes list. Initialised only
/// after we confirm the user is logged in (see [_checkLoginAndFetch]).

/// Future function to retrieve externally owned notes list
// static Future? _fetchExternalNotes;
late Future<NotesCallResult> _fetchExternalNotes;
Future<NotesCallResult>? _fetchOwnNotes;

/// Future function to retrieve externally owned notes list.

Future<NotesCallResult>? _fetchExternalNotes;

/// Tracks the user's login status. `null` while the asynchronous
/// check is in flight, then `true`/`false` once known.

bool? _isLoggedIn;

/// Scroll controller for single child scroll view
late final ScrollController _scrollController;
Expand All @@ -78,12 +86,30 @@ class _ListNotesScreenState extends State<ListNotesScreen> {
void initState() {
super.initState();
_scaffoldController = widget.scaffoldController;

_scrollController = ScrollController();
_checkLoginAndFetch();
}

/// Confirms the user is logged in before triggering the POD fetches.
///
/// When the user is not logged in we skip the fetches entirely and
/// let [build] render the [NotLoggedInCard] placeholder. This avoids
/// the cascade of layout exceptions that the fallback "no notes"
/// layout otherwise produces on an unauthenticated session.

// Set future functions to fetch owner's notes and external notes
_fetchOwnNotes = NoteService().getOwnNoteList();
_fetchExternalNotes = getExternalNoteList();
Future<void> _checkLoginAndFetch() async {
final loggedIn = await isUserLoggedIn();
if (!mounted) return;
setState(() {
_isLoggedIn = loggedIn;
if (loggedIn) {
_fetchOwnNotes = NoteService().getOwnNoteList();
_fetchExternalNotes = getExternalNoteList();
} else {
_fetchOwnNotes = null;
_fetchExternalNotes = null;
}
});
}

@override
Expand Down Expand Up @@ -190,16 +216,36 @@ class _ListNotesScreenState extends State<ListNotesScreen> {

@override
Widget build(BuildContext context) {
// Show the loading screen until the asynchronous login check has
// returned. Once we know the user is logged out, render the
// friendly `Not logged in` placeholder rather than attempting to
// fetch notes from a POD we cannot read.

if (_isLoggedIn == null) {
return Scaffold(
key: _scaffoldKey,
body: SafeArea(child: loadingScreen(normalLoadingScreenHeight)),
);
}
if (_isLoggedIn == false ||
_fetchOwnNotes == null ||
_fetchExternalNotes == null) {
return Scaffold(
key: _scaffoldKey,
body: const SafeArea(child: NotLoggedInCard()),
);
}

return Scaffold(
key: _scaffoldKey,
body: SafeArea(
child: FutureBuilder(
// future: _asyncFetchOwnNotes,
future: Future.wait([
// Future result of fetching owner's notes list
_fetchOwnNotes,
_fetchOwnNotes!,
// Future result of fetching externally owned notes list
_fetchExternalNotes,
_fetchExternalNotes!,
]),
builder: (context, snapshot) {
// if (!snapshot.hasData) {
Expand Down
123 changes: 123 additions & 0 deletions lib/widgets/not_logged_in_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/// `Not logged in` placeholder card for notes screens.
///
/// Copyright (C) 2026, Software Innovation Institute, ANU
///
/// Licensed under the GNU General Public License, Version 3 (the "License");
///
/// License: https://opensource.org/license/gpl-3-0
//
// Time-stamp: <Tuesday 2026-05-20 01:18:00 +1000 Tony Chen>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://opensource.org/license/gpl-3-0>.
///
/// Authors: Tony Chen

library;

import 'package:flutter/material.dart';

import 'package:solidui/solidui.dart' show SolidLoginRequiredDialog;

/// A friendly placeholder shown on note list screens when the user is
/// not logged in.
///
/// Rather than letting the screen try to fetch notes from the POD and
/// fall over with rendering or assertion errors, we present a clear
/// message and a `Log In` button. The button reuses solidui's shared
/// [SolidLoginRequiredDialog], which on confirmation navigates the
/// user back to the app's standard Solid login page.

class NotLoggedInCard extends StatelessWidget {
/// Message displayed in the body of the card.

final String message;

/// Optional default message tailored to the notes listing screens.

static const String defaultMessage =
'You need to be logged in to your POD to view and manage your notes.';

const NotLoggedInCard({super.key, this.message = defaultMessage});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Card(
elevation: 4,
margin: const EdgeInsets.all(24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
size: 56,
color: theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
'Not logged in',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
icon: const Icon(Icons.login),
label: const Text('Log In'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
),
onPressed: () => SolidLoginRequiredDialog.showAndHandle(
context,
message:
'Please log in to your POD to view and manage your '
'notes.',
),
),
],
),
),
),
),
);
}
}
Loading