From 08cccfb14e32061d30705775e3f4523f6b6156e9 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 15:29:10 +0530 Subject: [PATCH 1/9] Split Home page into own file as per KeyPod template #16 --- lib/home.dart | 38 ++++++++++++++++++++++++++++++++++++++ lib/main.dart | 38 -------------------------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index 54d62e6..3562786 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -26,6 +26,8 @@ library; import 'package:flutter/material.dart'; +import 'package:solidui/solidui.dart'; + import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -35,6 +37,42 @@ import 'package:innerpod/widgets/history.dart'; import 'package:innerpod/widgets/instructions.dart'; import 'package:innerpod/widgets/timer.dart'; +/// The primary widget for the app. + +class InnerPod extends StatelessWidget { + /// The primary app widget. + + const InnerPod({super.key}); + + @override + Widget build(BuildContext context) { + /// We wrap the actual home widget within a [SolidLogin]. If the app has + /// functionality that does not require access to Pod data then [required] + /// can be `false`. If the user connects to their Pod then we can ensure + /// their session information will be saved. If we aim to save the data to + /// the Pod or view data from the Pod, then if the user did not log i during + /// startup then we can call [SolidLoginPopup] to establish the connection + /// at that time. The login token and the security key are (optionally) + /// cached so that the login information is not required every time. + + return const SolidLogin( + title: 'MANAGE YOUR INNER POD', + required: false, + image: AssetImage('assets/images/inner_image.jpg'), + logo: AssetImage('assets/images/inner_icon.png'), + continueButtonStyle: ContinueButtonStyle( + text: 'Session', + background: Colors.lightGreenAccent, + ), + infoButtonStyle: InfoButtonStyle( + tooltip: 'Browse to the InnerPod home page.', + ), + link: 'https://github.com/gjwgit/innerpod/blob/dev/README.md', + child: Home(), + ); + } +} + /// A widget for the actuall app's main home page. class Home extends StatefulWidget { diff --git a/lib/main.dart b/lib/main.dart index 771e145..2995a56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,8 +27,6 @@ library; import 'package:flutter/material.dart'; -import 'package:solidui/solidui.dart'; - import 'package:innerpod/home.dart'; //import 'package:innerpod/timer.dart'; @@ -41,39 +39,3 @@ void main() { runApp(const MaterialApp(title: 'Inner Pod', home: InnerPod())); } - -/// The primary widget for the app. - -class InnerPod extends StatelessWidget { - /// The primary app widget. - - const InnerPod({super.key}); - - @override - Widget build(BuildContext context) { - /// We wrap the actual home widget within a [SolidLogin]. If the app has - /// functionality that does not require access to Pod data then [required] - /// can be `false`. If the user connects to their Pod then we can ensure - /// their session information will be saved. If we aim to save the data to - /// the Pod or view data from the Pod, then if the user did not log i during - /// startup then we can call [SolidLoginPopup] to establish the connection - /// at that time. The login token and the security key are (optionally) - /// cached so that the login information is not required every time. - - return const SolidLogin( - title: 'MANAGE YOUR INNER POD', - required: false, - image: AssetImage('assets/images/inner_image.jpg'), - logo: AssetImage('assets/images/inner_icon.png'), - continueButtonStyle: ContinueButtonStyle( - text: 'Session', - background: Colors.lightGreenAccent, - ), - infoButtonStyle: InfoButtonStyle( - tooltip: 'Browse to the InnerPod home page.', - ), - link: 'https://github.com/gjwgit/innerpod/blob/dev/README.md', - child: Home(), - ); - } -} From f52c73224f9187bcdab577736c81901288b1910b Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 15:42:05 +0530 Subject: [PATCH 2/9] Split About Dialog into to own file #17 --- lib/home.dart | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index 3562786..e079f8d 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -163,23 +163,6 @@ class HomeState extends State with SingleTickerProviderStateMixin { ), ), ), - // TODO 20250730 gjw MIGRATE TO VersionWidget - // - // I was not able to get this working - the value of _appVersion - // remains empty in the VersionWidget() even though it is correct in the - // Text(). - // - // Text('XX $_appVersion'), - // VersionWidget( - // version: 'YY $_appVersion', - // changelogUrl: _changelogUrl, - // showDate: false, - // userTextStyle: const TextStyle( - // color: Colors.deepPurple, - // fontSize: 10, - // ), - // ), - // Text('Version $_appVersion', style: const TextStyle(fontSize: 10)), const SizedBox(width: 50), IconButton( icon: const Icon(Icons.info), From 8ac430b54b92de73351fa51084cb4a6708ebd6b6 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 15:53:55 +0530 Subject: [PATCH 3/9] Fix import ordering in home.dart and main.dart #16 #17 --- lib/home.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/home.dart b/lib/home.dart index e079f8d..f4b5409 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -26,9 +26,8 @@ library; import 'package:flutter/material.dart'; -import 'package:solidui/solidui.dart'; - import 'package:package_info_plus/package_info_plus.dart'; +import 'package:solidui/solidui.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:innerpod/constants/colours.dart'; From a2dde1f22b18509a5f087f9fe8d018ac6df008e2 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:37:11 +0530 Subject: [PATCH 4/9] Fix session saving and AnimationController errors --- lib/widgets/history.dart | 38 ++++++++++++++++++-------------------- lib/widgets/timer.dart | 27 +++++++++++++-------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index c44418d..d51204c 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -42,34 +42,32 @@ class _HistoryState extends State { String? content; try { content = await readPod('sessions.ttl'); - } catch (e) { + } on ResourceNotExistException { // If file doesn't exist yet, treat as empty (no sessions) - debugPrint('sessions.ttl does not exist yet: $e'); + debugPrint('sessions.ttl does not exist yet (normal for new users)'); + content = null; + } catch (e) { + debugPrint('Error reading from Pod: $e'); content = null; } // parseSessions handles null content and returns empty list List jsonList = parseSessions(content); - if (jsonList.isNotEmpty) { - 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(); - }); - } + 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) - debugPrint( - 'sessions.ttl does not exist yet (this is normal for new users)', - ); + debugPrint('Unexpected error loading sessions: $e'); } finally { if (mounted) { setState(() { diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 2654fb1..c98cd63 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -245,33 +245,29 @@ class TimerState extends State { // What to do at the end of a session. logMessage('Session Completed'); + + // Only play audio and wait if still mounted if (mounted) { await _player.play(dong); debugPrint('COMPLETE: dong waiting: $_audioDuration'); await Future.delayed(_audioDuration); } - if (!mounted) { - await _saveSession(); - return; - } - - if (_isGuided) { + // Check mounted state again after the delay + if (mounted && _isGuided) { await _player.play(sessionOutro); debugPrint('COMPLETE: outro waiting: $_audioDuration'); await Future.delayed(_audioDuration); - if (!mounted) { - await _saveSession(); - return; - } } - // Check if widget is still mounted before calling _reset() - // to avoid AnimationController errors after disposal + // Reset controls only if still mounted to avoid AnimationController errors if (mounted) { _reset(); _allowSleep(); } + + // Always attempt to save the session, even if navigate away + // _saveSession handles its own internal null checks await _saveSession(); } @@ -288,9 +284,12 @@ class TimerState extends State { String? content; try { content = await readPod('sessions.ttl'); + } on ResourceNotExistException { + // File doesn't exist yet, we'll create it. + debugPrint('sessions.ttl does not exist, creating new file.'); + content = null; } catch (e) { - // If the file does not exist (e.g., first session), we treat it as - // null. + logMessage('Error reading sessions.ttl: $e'); content = null; } String newContent = addSession(content, session); From 02383d4cb28b846211f04fc17ba2a5a328efce57 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:44:21 +0530 Subject: [PATCH 5/9] Record Session Type and Silence Duration #42 --- lib/utils/session_logic.dart | 15 +++++++++++++-- lib/widgets/history.dart | 7 +++++++ lib/widgets/timer.dart | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/utils/session_logic.dart b/lib/utils/session_logic.dart index 4e64b8c..7284ac3 100644 --- a/lib/utils/session_logic.dart +++ b/lib/utils/session_logic.dart @@ -46,9 +46,11 @@ List> parseSessions(String? content) { final RegExp sessionBlockRegExp = RegExp(r':session_\d+.*?\.(?:\s+|$)', dotAll: true); - // RegExp to extract start and end times within a block + // RegExp to extract properties within a block final RegExp startRegExp = RegExp(r':start "(.*?)"\^\^xsd:dateTime'); final RegExp endRegExp = RegExp(r':end "(.*?)"\^\^xsd:dateTime'); + final RegExp typeRegExp = RegExp(r':type "(.*?)"'); + final RegExp durationRegExp = RegExp(r':silenceDuration (\d+)'); final matches = sessionBlockRegExp.allMatches(content); @@ -56,11 +58,15 @@ List> parseSessions(String? content) { final block = match.group(0)!; final startMatch = startRegExp.firstMatch(block); final endMatch = endRegExp.firstMatch(block); + final typeMatch = typeRegExp.firstMatch(block); + final durationMatch = durationRegExp.firstMatch(block); if (startMatch != null && endMatch != null) { sessions.add({ 'start': startMatch.group(1)!, 'end': endMatch.group(1)!, + 'type': typeMatch?.group(1) ?? 'basic', + 'silenceDuration': durationMatch?.group(1) ?? '1200', // Default to 20m }); } } @@ -87,6 +93,9 @@ String addSession(String? currentContent, Map newSession) { final String start = newSession['start']; final String end = newSession['end']; + final String type = newSession['type'] ?? 'basic'; + final int silenceDuration = newSession['silenceDuration'] ?? 1200; + // Use timestamp as unique ID final String id = DateTime.parse(start).millisecondsSinceEpoch.toString(); @@ -97,7 +106,9 @@ String addSession(String? currentContent, Map newSession) { $separator :session_$id a :Session; :start "$start"^^xsd:dateTime; - :end "$end"^^xsd:dateTime. + :end "$end"^^xsd:dateTime; + :type "$type"; + :silenceDuration $silenceDuration. '''; return content + newEntry; diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index d51204c..76b3759 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -62,6 +62,9 @@ class _HistoryState extends State { 'date': DateFormat('yyyy-MM-dd').format(start), 'start': DateFormat('HH:mm:ss').format(start), 'end': DateFormat('HH:mm:ss').format(end), + 'type': (item['type'] ?? 'basic') as String, + 'duration': + '${(int.parse(item['silenceDuration'] ?? '1200') / 60).round()}m', }; }).toList(); }); @@ -102,6 +105,8 @@ class _HistoryState extends State { child: DataTable( columns: const [ DataColumn(label: Text('Date')), + DataColumn(label: Text('Type')), + DataColumn(label: Text('Min')), DataColumn(label: Text('Start')), DataColumn(label: Text('End')), ], @@ -109,6 +114,8 @@ class _HistoryState extends State { return DataRow( cells: [ DataCell(Text(session['date']!)), + DataCell(Text(session['type']!)), + DataCell(Text(session['duration']!)), DataCell(Text(session['start']!)), DataCell(Text(session['end']!)), ], diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index c98cd63..db288ea 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -83,6 +83,10 @@ class TimerState extends State { DateTime? _startTime; + // Track the session type. + + String _sessionType = 'basic'; + //////////////////////////////////////////////////////////////////////// // CONSTANTS //////////////////////////////////////////////////////////////////////// @@ -155,6 +159,7 @@ class TimerState extends State { _reset(); _stopSleep(); _isGuided = false; + _sessionType = 'intro'; _startTime = DateTime.now(); // Good to wait a second before starting the audio after tapping the button, @@ -204,6 +209,7 @@ class TimerState extends State { _reset(); _stopSleep(); _isGuided = true; + _sessionType = 'guided'; _startTime = DateTime.now(); // Good to wait a second before starting the audio after tapping the button, @@ -278,6 +284,8 @@ class TimerState extends State { final session = { 'start': _startTime!.toIso8601String(), 'end': endTime.toIso8601String(), + 'type': _sessionType, + 'silenceDuration': _duration, }; try { @@ -333,6 +341,7 @@ minutes, beginning and ending with three chimes. dingDong(_player); _controller.restart(); _stopSleep(); + _sessionType = 'basic'; _startTime = DateTime.now(); }, fontWeight: FontWeight.bold, From 5a88df37906071d85b642f8a48c0143792d99828 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:45:55 +0530 Subject: [PATCH 6/9] Fix sequential audio playback issues on Ubuntu #30 --- lib/widgets/timer.dart | 75 +++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index db288ea..aca740d 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -75,10 +75,6 @@ class TimerState extends State { var _duration = defaultSessionSeconds; - // Track the duration of a loaded audio file. - - var _audioDuration = Duration.zero; - // Track the start time of a session. DateTime? _startTime; @@ -102,10 +98,6 @@ class TimerState extends State { final _player = AudioPlayer(); - // Subscription for audio duration changes. - - StreamSubscription? _durationSubscription; - //////////////////////////////////////////////////////////////////////// // SLEEP //////////////////////////////////////////////////////////////////////// @@ -121,21 +113,27 @@ class TimerState extends State { @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(); } + /// Helper to play an audio source and wait for it to complete. + Future _play(Source source) async { + if (!mounted) return; + try { + await _player.stop(); + await _player.play(source); + // Wait for the audio to finish playing. + await _player.onPlayerComplete.first; + } catch (e) { + debugPrint('Audio playback error or interrupted: $e'); + } + } + //////////////////////////////////////////////////////////////////////// // RESET //////////////////////////////////////////////////////////////////////// @@ -168,20 +166,8 @@ class TimerState extends State { 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'); - - // Wait now while the intro audio is played before the dong when the timer - // then actually starts. - - //await Future.delayed(Duration(seconds: _introTime)); - await Future.delayed(_audioDuration); + // Play and wait for the intro audio. + await _play(introAudio); if (!mounted) return; // Good to wait another 1 second here before the dings after the @@ -190,7 +176,7 @@ class TimerState extends State { await Future.delayed(const Duration(seconds: 1)); if (!mounted) return; - await dingDong(_player); + await _play(dong); if (!mounted) return; _controller.restart(); } @@ -219,13 +205,7 @@ class TimerState extends State { if (!mounted) return; // Play and wait for the session guide audio to finish. - - await _player.stop(); - await _player.play(sessionGuide); - - debugPrint('GUIDED: guide waiting $_audioDuration'); - - await Future.delayed(_audioDuration); + await _play(sessionGuide); if (!mounted) return; // Good to wait a second before the dings otherwise it feels rushed coming @@ -234,11 +214,9 @@ class TimerState extends State { 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. + // The introductions are complete. We play the dings and start the timer. - await dingDong(_player); - debugPrint('GUIDED: dong waiting $_audioDuration'); + await _play(dong); if (!mounted) return; _controller.restart(); } @@ -254,16 +232,17 @@ class TimerState extends State { // Only play audio and wait if still mounted if (mounted) { - await _player.play(dong); - debugPrint('COMPLETE: dong waiting: $_audioDuration'); - await Future.delayed(_audioDuration); + await _play(dong); } - // Check mounted state again after the delay + // Check mounted state again after the dings if (mounted && _isGuided) { - await _player.play(sessionOutro); - debugPrint('COMPLETE: outro waiting: $_audioDuration'); - await Future.delayed(_audioDuration); + // Add a small delay between the dings and the outro music for smoother transition + // especially on systems with busy audio pipes (like Linux with audio sharing). + await Future.delayed(const Duration(milliseconds: 500)); + if (mounted) { + await _play(sessionOutro); + } } // Reset controls only if still mounted to avoid AnimationController errors From dfe0f3405dd3ddee8a29a6578563f9010f7ccb87 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:52:08 +0530 Subject: [PATCH 7/9] Document blue visual cue for active session #27 --- README.md | 4 +++- lib/widgets/about.dart | 6 ++++-- lib/widgets/timer.dart | 8 +++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cfbf4e2..30ed318 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ and previous sessions will be available for visualising. Tap on the **INFO** button to review this guide. Once you connect to the app the session manager displays a countdown -timer and buttons to interact and manage the session. +timer and buttons to interact and manage the session. As the timer +progresses, the circular progress bar fills with blue, providing a visual +cue that the session and audio are active. ![App Home Screen](screenshots/app_home_screen.png) diff --git a/lib/widgets/about.dart b/lib/widgets/about.dart index 643a10d..0e11e69 100644 --- a/lib/widgets/about.dart +++ b/lib/widgets/about.dart @@ -44,8 +44,10 @@ Future showAppAboutDialog(BuildContext context) async { 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. + is the tradition in many cultures and religions. The blue progress circle + provides a visual cue that the session is active and audio may be + playing. 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. diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index aca740d..28b209e 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -310,7 +310,8 @@ class TimerState extends State { tooltip: ''' Tap here to begin a session of silence for ${(_duration / 60).round()} -minutes, beginning and ending with three chimes. +minutes, beginning and ending with three chimes. The blue progress +circle indicates an active session. ''' .trim(), @@ -374,7 +375,7 @@ of the Resume button. Tap here to play a short introduction for a session. After the introduction a ${(_duration / 60).round()} minute session of silence will begin and end with -three dings. +three dings. The blue progress circle indicates an active session. ''' .trim(), @@ -390,7 +391,8 @@ three dings. Tap here to play a ${10 + (_duration / 60).round()} minute guided session. The session begins with instructions for meditation from John Main. Introductory music is followed by three chimes and a ${(_duration / 60).round()} -minute silent session which is then finished with another three chimes. The +minute silent session which is then finished with another three chimes. The +blue progress circle indicates an active session. The audio may take a little time to download for the Web version. ''' From 3167cf2f7c9e0245ce3f2dd3a0085b1cbfc29896 Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:54:39 +0530 Subject: [PATCH 8/9] Fix trailing space in README.md linter error #27 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30ed318..732eee9 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Tap on the **INFO** button to review this guide. Once you connect to the app the session manager displays a countdown timer and buttons to interact and manage the session. As the timer -progresses, the circular progress bar fills with blue, providing a visual +progresses, the circular progress bar fills with blue, providing a visual cue that the session and audio are active. ![App Home Screen](screenshots/app_home_screen.png) From 092062276959e22796471bceae63f5805c3a3a2f Mon Sep 17 00:00:00 2001 From: Amogh Hosamane Date: Fri, 13 Feb 2026 23:58:38 +0530 Subject: [PATCH 9/9] Fix markdown lint errors in ios README.md --- ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md index 89c2725..91f3c0d 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -1,5 +1,8 @@ # Launch Screen Assets -You can customize the launch screen with your own desired assets by replacing the image files in this directory. +You can customize the launch screen with your own desired assets by replacing +the image files in this directory. -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file +You can also do it by opening your Flutter project's Xcode project with +`open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the +Project Navigator and dropping in the desired images.