diff --git a/lib/common/rest_api/file_helper.dart b/lib/common/rest_api/file_helper.dart index b04cbc8..f3c5734 100644 --- a/lib/common/rest_api/file_helper.dart +++ b/lib/common/rest_api/file_helper.dart @@ -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 @@ -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; + } } } } diff --git a/lib/notes/list_my_notes_screen.dart b/lib/notes/list_my_notes_screen.dart index 4e824aa..dae7fd0 100644 --- a/lib/notes/list_my_notes_screen.dart +++ b/lib/notes/list_my_notes_screen.dart @@ -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'; @@ -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 @@ -59,7 +61,12 @@ class _ListMyNotesScreenState extends State { final GlobalKey _scaffoldKey = GlobalKey(); /// Future function to retrieve user's notes list - static Future? _asyncDataFetch; + Future? _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; @@ -71,8 +78,24 @@ class _ListMyNotesScreenState extends State { 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 _checkLoginAndFetch() async { + final loggedIn = await isUserLoggedIn(); + if (!mounted) return; + setState(() { + _isLoggedIn = loggedIn; + _asyncDataFetch = loggedIn ? NoteService().getOwnNoteList() : null; + }); } @override @@ -151,6 +174,24 @@ class _ListMyNotesScreenState extends State { @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( diff --git a/lib/notes/list_notes_screen.dart b/lib/notes/list_notes_screen.dart index 3dea414..6a8e0f9 100644 --- a/lib/notes/list_notes_screen.dart +++ b/lib/notes/list_notes_screen.dart @@ -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'; @@ -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'; @@ -60,13 +62,19 @@ class ListNotesScreen extends StatefulWidget { class _ListNotesScreenState extends State { final GlobalKey _scaffoldKey = GlobalKey(); - /// Future function to retrieve user's notes list - // static Future? _fetchOwnNotes; - late Future _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 _fetchExternalNotes; + Future? _fetchOwnNotes; + + /// Future function to retrieve externally owned notes list. + + Future? _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; @@ -78,12 +86,30 @@ class _ListNotesScreenState extends State { 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 _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 @@ -190,6 +216,26 @@ class _ListNotesScreenState extends State { @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( @@ -197,9 +243,9 @@ class _ListNotesScreenState extends State { // 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) { diff --git a/lib/widgets/not_logged_in_card.dart b/lib/widgets/not_logged_in_card.dart new file mode 100644 index 0000000..906c0a7 --- /dev/null +++ b/lib/widgets/not_logged_in_card.dart @@ -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: +// +// 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 . +/// +/// 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.', + ), + ), + ], + ), + ), + ), + ), + ); + } +}