Skip to content
Open
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 5 additions & 2 deletions ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
Original file line number Diff line number Diff line change
@@ -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.
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.
54 changes: 37 additions & 17 deletions lib/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ library;
import 'package:flutter/material.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';
Expand All @@ -35,6 +36,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 {
Expand Down Expand Up @@ -125,23 +162,6 @@ class HomeState extends State<Home> 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),
Expand Down
38 changes: 0 additions & 38 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(),
);
}
}
15 changes: 13 additions & 2 deletions lib/utils/session_logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,27 @@ List<Map<String, String>> 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);

for (final match in matches) {
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
});
}
}
Expand All @@ -87,6 +93,9 @@ String addSession(String? currentContent, Map<String, dynamic> 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();

Expand All @@ -97,7 +106,9 @@ String addSession(String? currentContent, Map<String, dynamic> 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;
Expand Down
6 changes: 4 additions & 2 deletions lib/widgets/about.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ Future<void> 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.
Expand Down
45 changes: 25 additions & 20 deletions lib/widgets/history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,34 +42,35 @@ class _HistoryState extends State<History> {
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<dynamic> 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),
'type': (item['type'] ?? 'basic') as String,
'duration':
'${(int.parse(item['silenceDuration'] ?? '1200') / 60).round()}m',
};
}).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(() {
Expand Down Expand Up @@ -104,13 +105,17 @@ class _HistoryState extends State<History> {
child: DataTable(
columns: const [
DataColumn(label: Text('Date')),
DataColumn(label: Text('Type')),
DataColumn(label: Text('Min')),
DataColumn(label: Text('Start')),
DataColumn(label: Text('End')),
],
rows: _sessions.map((session) {
return DataRow(
cells: [
DataCell(Text(session['date']!)),
DataCell(Text(session['type']!)),
DataCell(Text(session['duration']!)),
DataCell(Text(session['start']!)),
DataCell(Text(session['end']!)),
],
Expand Down
Loading