From c65d4b2c43693e4d2135554b06912093c20e7c93 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 20:16:53 +0530 Subject: [PATCH 1/6] Support landscape mode #7 --- .lycheeignore | 2 + lib/widgets/history.dart | 5 +- lib/widgets/timer.dart | 166 +++++++++++++++++++++++++-------------- 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/.lycheeignore b/.lycheeignore index 58bd48b..d69b336 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -45,6 +45,8 @@ web/** # Placeholder URLs in documentation https://github.com/YOUR_USERNAME/innerpod.git +https://github.com/gjwgit/innerpod/blob/dev/README.md + # Licenses. Note that gnu.org is throttled so do not check it. diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index 18ad894..1bf308e 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -54,7 +54,10 @@ class _HistoryState extends State { }); } } catch (e) { - debugPrint('Error loading sessions: $e'); + // If file doesn't exist yet, treat as empty (no sessions) + debugPrint( + 'sessions.ttl does not exist yet (this is normal for new users)', + ); } finally { setState(() { _isLoading = false; diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 486f17e..0516375 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -109,6 +109,12 @@ class TimerState extends State { void _stopSleep() => WakelockPlus.enable(); + @override + void dispose() { + _player.dispose(); + super.dispose(); + } + //////////////////////////////////////////////////////////////////////// // RESET //////////////////////////////////////////////////////////////////////// @@ -128,6 +134,7 @@ class TimerState extends State { // An audio is played and then we begin the session. logMessage('Start Intro Session'); + if (!mounted) return; _reset(); _stopSleep(); _isGuided = false; @@ -137,11 +144,13 @@ class TimerState extends State { // otherwise it feels rushed. await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return; // Make sure there is no other audio playing just now and then start the // intro audio. await _player.stop(); + if (!mounted) return; await _player.play(introAudio); debugPrint('INTRO: intro waiting $_audioDuration'); @@ -151,13 +160,16 @@ class TimerState extends State { //await Future.delayed(Duration(seconds: _introTime)); await Future.delayed(_audioDuration); + if (!mounted) return; // Good to wait another 1 second here before the dings after the // introduction audio, otherwise it feels rushed. await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return; await dingDong(_player); + if (!mounted) return; _controller.restart(); } @@ -171,6 +183,7 @@ class TimerState extends State { // another musical interlude. logMessage('Start Guided Session'); + if (!mounted) return; _reset(); _stopSleep(); _isGuided = true; @@ -180,6 +193,7 @@ class TimerState extends State { // otherwise it feels rushed. await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return; // Play and wait for the session guide audio to finish. @@ -189,17 +203,20 @@ class TimerState extends State { debugPrint('GUIDED: guide waiting $_audioDuration'); await Future.delayed(_audioDuration); + if (!mounted) return; // Good to wait a second before the dings otherwise it feels rushed coming // straight from the music. await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return; // The introductions are complete. We now tell the device not to sleep, play // the dings, and start the timer. await dingDong(_player); debugPrint('GUIDED: dong waiting $_audioDuration'); + if (!mounted) return; _controller.restart(); } @@ -211,18 +228,33 @@ class TimerState extends State { // What to do at the end of a session. logMessage('Session Completed'); - await _player.play(dong); - debugPrint('COMPLETE: dong waiting: $_audioDuration'); - await Future.delayed(_audioDuration); + if (mounted) { + await _player.play(dong); + debugPrint('COMPLETE: dong waiting: $_audioDuration'); + await Future.delayed(_audioDuration); + } + + if (!mounted) { + await _saveSession(); + return; + } if (_isGuided) { await _player.play(sessionOutro); debugPrint('COMPLETE: outro waiting: $_audioDuration'); await Future.delayed(_audioDuration); + if (!mounted) { + await _saveSession(); + return; + } } - _reset(); - _allowSleep(); + // Check if widget is still mounted before calling _reset() + // to avoid AnimationController errors after disposal + if (mounted) { + _reset(); + _allowSleep(); + } await _saveSession(); } @@ -399,64 +431,80 @@ audio may take a little time to download for the Web version. // RETURN //////////////////////////////////// - return SingleChildScrollView( - child: Padding( - // Add some top and bottom padding so the timer is not clipped at the - // top nor the chips at the bottom. - padding: const EdgeInsets.only(top: 10, bottom: 5), - child: Column( + final timerDisplay = AppCircularCountDownTimer( + duration: _duration, + controller: _controller, + onComplete: _complete, + ); + + final buttonsMatrix = Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - AppCircularCountDownTimer( - duration: _duration, - controller: _controller, - onComplete: _complete, - ), - const SizedBox(height: 2 * heightSpacer), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - introButton, - const SizedBox(width: widthSpacer), - startButton, - ], - ), - const SizedBox(height: heightSpacer), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - guidedButton, - const SizedBox(width: widthSpacer), - pauseButton, - ], - ), - // TODO 20240707 gjw EVENTUALLY PUT RESUME AND RESET INTO PAUSE. - // - // A long press will be RESET. On tap PASUE turn the button - // into RESUME. - // - // const SizedBox(height: heightSpacer), - // Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // resetButton, - // const SizedBox(width: widthSpacer), - // resumeButton, - // ], - // ), - const SizedBox(height: 2 * heightSpacer), - const Text( - 'Select duration (minutes)', - style: TextStyle(fontSize: 20.0, color: Colors.grey), - ), - const SizedBox(height: 1 * heightSpacer), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [durationChoice], - ), + introButton, + const SizedBox(width: widthSpacer), + startButton, ], ), - ), + const SizedBox(height: heightSpacer), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + guidedButton, + const SizedBox(width: widthSpacer), + pauseButton, + ], + ), + const SizedBox(height: 2 * heightSpacer), + const Text( + 'Select duration (minutes)', + style: TextStyle(fontSize: 20.0, color: Colors.grey), + ), + const SizedBox(height: 1 * heightSpacer), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [durationChoice], + ), + ], + ); + + return OrientationBuilder( + builder: (context, orientation) { + if (orientation == Orientation.portrait) { + return SingleChildScrollView( + child: Padding( + // Add some top and bottom padding so the timer is not clipped at the + // top nor the chips at the bottom. + padding: const EdgeInsets.only(top: 10, bottom: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + timerDisplay, + const SizedBox(height: 2 * heightSpacer), + buttonsMatrix, + ], + ), + ), + ); + } else { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Center(child: timerDisplay)), + const SizedBox(width: 2 * widthSpacer), + Expanded(child: Center(child: buttonsMatrix)), + ], + ), + ), + ); + } + }, ); } } From 0bf64cbb2ab965e89dd213fbc5b6b3412b47ac58 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 20:34:06 +0530 Subject: [PATCH 2/6] Support landscape mode (#7) and fix session saving/disposal errors (#79) --- lib/home.dart | 8 +++++--- lib/widgets/history.dart | 40 +++++++++++++++++++++++----------------- lib/widgets/timer.dart | 36 ++++++++++++++++++++++++++---------- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index d6094e2..766bcb8 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -82,9 +82,11 @@ class HomeState extends State with SingleTickerProviderStateMixin { Future _loadAppInfo() async { final packageInfo = await PackageInfo.fromPlatform(); - setState(() { - _appVersion = packageInfo.version; // Set app version from package info - }); + if (mounted) { + setState(() { + _appVersion = packageInfo.version; // Set app version from package info + }); + } } @override diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index 1bf308e..60d7f13 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -32,26 +32,30 @@ class _HistoryState extends State { } Future _loadSessions() async { - setState(() { - _isLoading = true; - }); + if (mounted) { + setState(() { + _isLoading = true; + }); + } try { String? content = await readPod('sessions.ttl'); // If readPod returns nullable, use ?. or just pass to parseSessions which handles null List jsonList = parseSessions(content); if (jsonList.isNotEmpty) { - setState(() { - _sessions = jsonList.map((item) { - final start = DateTime.parse(item['start']); - final end = DateTime.parse(item['end']); - return { - 'date': DateFormat('yyyy-MM-dd').format(start), - 'start': DateFormat('HH:mm:ss').format(start), - 'end': DateFormat('HH:mm:ss').format(end), - }; - }).toList(); - }); + if (mounted) { + setState(() { + _sessions = jsonList.map((item) { + final start = DateTime.parse(item['start']); + final end = DateTime.parse(item['end']); + return { + 'date': DateFormat('yyyy-MM-dd').format(start), + 'start': DateFormat('HH:mm:ss').format(start), + 'end': DateFormat('HH:mm:ss').format(end), + }; + }).toList(); + }); + } } } catch (e) { // If file doesn't exist yet, treat as empty (no sessions) @@ -59,9 +63,11 @@ class _HistoryState extends State { 'sessions.ttl does not exist yet (this is normal for new users)', ); } finally { - setState(() { - _isLoading = false; - }); + if (mounted) { + setState(() { + _isLoading = false; + }); + } } } diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 0516375..120f885 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -25,6 +25,8 @@ library; +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; @@ -97,6 +99,10 @@ class TimerState extends State { final _player = AudioPlayer(); + // Subscription for audio duration changes. + + StreamSubscription? _durationSubscription; + //////////////////////////////////////////////////////////////////////// // SLEEP //////////////////////////////////////////////////////////////////////// @@ -109,8 +115,20 @@ class TimerState extends State { void _stopSleep() => WakelockPlus.enable(); + @override + void initState() { + super.initState(); + + // Listen to the duration of the audio file being played. + + _durationSubscription = _player.onDurationChanged.listen((d) { + _audioDuration = d; + }); + } + @override void dispose() { + _durationSubscription?.cancel(); _player.dispose(); super.dispose(); } @@ -268,7 +286,14 @@ class TimerState extends State { }; try { - String? content = await readPod('sessions.ttl'); + String? content; + try { + content = await readPod('sessions.ttl'); + } catch (e) { + // If the file does not exist (e.g., first session), we treat it as + // null. + content = null; + } String newContent = addSession(content, session); await writePod('sessions.ttl', newContent); logMessage('Session saved to Pod'); @@ -287,15 +312,6 @@ class TimerState extends State { Widget build(BuildContext context) { // Build the Timer Widget. - // Add a listener for a change in the duration of the playing audio - // file. When the audio is loaded from file then take note of the duration - // of the audio which will then be used to pause until the audio is - // complete. 20240329 gjw - - _player.onDurationChanged.listen((d) { - _audioDuration = d; - }); - //////////////////////////////////// // APP BUTTONS //////////////////////////////////// From 0ff31e2153eb918b781e6e788baffd42329f1bc6 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 20:41:01 +0530 Subject: [PATCH 3/6] Split About Dialog into own file (#17) --- lib/home.dart | 58 +---------------------------- lib/widgets/about.dart | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 56 deletions(-) create mode 100644 lib/widgets/about.dart diff --git a/lib/home.dart b/lib/home.dart index 766bcb8..4909dd8 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -27,14 +27,11 @@ library; import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; -import 'package:gap/gap.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:solidpod/solidpod.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:innerpod/constants/colours.dart'; -import 'package:innerpod/utils/word_wrap.dart'; +import 'package:innerpod/widgets/about.dart'; import 'package:innerpod/widgets/history.dart'; import 'package:innerpod/widgets/instructions.dart'; import 'package:innerpod/widgets/timer.dart'; @@ -60,24 +57,6 @@ class HomeState extends State with SingleTickerProviderStateMixin { final String _changelogUrl = 'https://github.com/gjwgit/innerpod/blob/dev/CHANGELOG.md'; - final _about = wordWrap(''' - - Inner Pod is an app for timing meditation sessions and, optionally, storing a - log of your meditation sessions to your Pod. A session, in fact, can be - anything. The app is commonly used for contemplative or silent meditation as - is the tradition in many cultures and religions. The concept for the app and - images were generated by large language models. - - The app is written in Flutter and the open source code is available from - **github**. You can also run the app **online** through your browser. - - **GitHub** [https://github.com/gjwgit/innerpod](https://github.com/gjwgit/innerpod) - - **OnLine** [https://innerpod.solidcommunity.au](https://innerpod.solidcommunity.au) - - **Author** [Graham Williams](https://togaware.com/graham.williams.html) - '''); - // Helper function to load the app name and version. Future _loadAppInfo() async { @@ -167,40 +146,7 @@ class HomeState extends State with SingleTickerProviderStateMixin { const SizedBox(width: 50), IconButton( icon: const Icon(Icons.info), - onPressed: () async { - final appInfo = await getAppNameVersion(); - final appName = appInfo.name; - - // Note the use of the conditional with `context.mounted` to avoid - // the "Don't use 'BuildContext's across async gaps" warning. - - if (context.mounted) { - showAboutDialog( - context: context, - applicationIcon: Image.asset( - 'assets/images/inner_icon.png', - width: 100, - height: 100, - ), - applicationName: - '${appName[0].toUpperCase()}${appName.substring(1)}', - applicationVersion: appInfo.version, - applicationLegalese: '© 2024 Togaware', - children: [ - const Gap(20), - MarkdownBody( - data: _about, - selectable: true, - softLineBreak: true, - onTapLink: (text, href, about) { - final url = Uri.parse(href ?? ''); - launchUrl(url); - }, - ), - ], - ); - } - }, + onPressed: () => showAppAboutDialog(context), tooltip: 'Popup a window about the app.', ), ], diff --git a/lib/widgets/about.dart b/lib/widgets/about.dart new file mode 100644 index 0000000..76ecd2b --- /dev/null +++ b/lib/widgets/about.dart @@ -0,0 +1,84 @@ +/// About dialog for the app. +// +// Time-stamp: <2026-02-12 20:40:00 Graham Williams> +// +/// Copyright (C) 2024-2026, Togaware Pty Ltd +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"); +/// +/// License: https://opensource.org/license/gpl-3-0 +// +// 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: Graham Williams + +import 'package:flutter/material.dart'; + +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:gap/gap.dart'; +import 'package:solidpod/solidpod.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:innerpod/utils/word_wrap.dart'; + +/// Display the about dialog for the app. + +Future showAppAboutDialog(BuildContext context) async { + final appInfo = await getAppNameVersion(); + final appName = appInfo.name; + + final about = wordWrap(''' + + Inner Pod is an app for timing meditation sessions and, optionally, storing a + log of your meditation sessions to your Pod. A session, in fact, can be + anything. The app is commonly used for contemplative or silent meditation as + is the tradition in many cultures and religions. The concept for the app and + images were generated by large language models. + + The app is written in Flutter and the open source code is available from + **github**. You can also run the app **online** through your browser. + + **GitHub** [https://github.com/gjwgit/innerpod](https://github.com/gjwgit/innerpod) + + **OnLine** [https://innerpod.solidcommunity.au](https://innerpod.solidcommunity.au) + + **Author** [Graham Williams](https://togaware.com/graham.williams.html) + '''); + + if (context.mounted) { + showAboutDialog( + context: context, + applicationIcon: Image.asset( + 'assets/images/inner_icon.png', + width: 100, + height: 100, + ), + applicationName: '${appName[0].toUpperCase()}${appName.substring(1)}', + applicationVersion: appInfo.version, + applicationLegalese: '© 2024 Togaware', + children: [ + const Gap(20), + MarkdownBody( + data: about, + selectable: true, + softLineBreak: true, + onTapLink: (text, href, about) { + final url = Uri.parse(href ?? ''); + launchUrl(url); + }, + ), + ], + ); + } +} From 9a06541dcc0ca91d86962c7f3b6dcdbae7c514bd Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 20:45:28 +0530 Subject: [PATCH 4/6] Fix dangling library doc comment lint in about.dart --- lib/widgets/about.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/about.dart b/lib/widgets/about.dart index 76ecd2b..f1a8ead 100644 --- a/lib/widgets/about.dart +++ b/lib/widgets/about.dart @@ -23,6 +23,8 @@ /// /// Authors: Graham Williams +library; + import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; From 0c1f14626ddfe79f43671e7dc17b35439d3ba498 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 21:03:13 +0530 Subject: [PATCH 5/6] Fix dangling library doc comment lints by removing blank lines before library directive --- lib/home.dart | 1 - lib/widgets/about.dart | 1 - lib/widgets/timer.dart | 1 - 3 files changed, 3 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index 4909dd8..fbbb80e 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -22,7 +22,6 @@ // this program. If not, see . /// /// Authors: Graham Williams - library; import 'package:flutter/material.dart'; diff --git a/lib/widgets/about.dart b/lib/widgets/about.dart index f1a8ead..f35db55 100644 --- a/lib/widgets/about.dart +++ b/lib/widgets/about.dart @@ -22,7 +22,6 @@ // this program. If not, see . /// /// Authors: Graham Williams - library; import 'package:flutter/material.dart'; diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 120f885..3f2fc68 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -22,7 +22,6 @@ // this program. If not, see . /// /// Authors: Graham Williams - library; import 'dart:async'; From 3c48c68fe5f183276da899d34364e2d0571d6a71 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Thu, 12 Feb 2026 21:04:29 +0530 Subject: [PATCH 6/6] Convert dangling library doc comments to regular comments in headers --- lib/home.dart | 12 ++++++------ lib/widgets/about.dart | 12 ++++++------ lib/widgets/timer.dart | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index fbbb80e..54d62e6 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -1,12 +1,12 @@ -/// A session timer with session logged to your Solid Pod. +// A session timer with session logged to your Solid Pod. // // Time-stamp: // -/// Copyright (C) 2024-2025, Togaware Pty Ltd -/// -/// Licensed under the GNU General Public License, Version 3 (the "License"); -/// -/// License: https://opensource.org/license/gpl-3-0 +// Copyright (C) 2024-2025, Togaware Pty Ltd +// +// Licensed under the GNU General Public License, Version 3 (the "License"); +// +// License: https://opensource.org/license/gpl-3-0 // // 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 diff --git a/lib/widgets/about.dart b/lib/widgets/about.dart index f35db55..643a10d 100644 --- a/lib/widgets/about.dart +++ b/lib/widgets/about.dart @@ -1,12 +1,12 @@ -/// About dialog for the app. +// About dialog for the app. // // Time-stamp: <2026-02-12 20:40:00 Graham Williams> // -/// Copyright (C) 2024-2026, Togaware Pty Ltd -/// -/// Licensed under the GNU General Public License, Version 3 (the "License"); -/// -/// License: https://opensource.org/license/gpl-3-0 +// Copyright (C) 2024-2026, Togaware Pty Ltd +// +// Licensed under the GNU General Public License, Version 3 (the "License"); +// +// License: https://opensource.org/license/gpl-3-0 // // 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 diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 3f2fc68..2654fb1 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -1,12 +1,12 @@ -/// A countdown timer and buttons for a session. +// A countdown timer and buttons for a session. // // Time-stamp: // -/// Copyright (C) 2024, Togaware Pty Ltd -/// -/// Licensed under the GNU General Public License, Version 3 (the "License"); -/// -/// License: https://opensource.org/license/gpl-3-0 +// Copyright (C) 2024, Togaware Pty Ltd +// +// Licensed under the GNU General Public License, Version 3 (the "License"); +// +// License: https://opensource.org/license/gpl-3-0 // // 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