diff --git a/.agent/workflows/setup-flutter.md b/.agent/workflows/setup-flutter.md new file mode 100644 index 0000000..292c23d --- /dev/null +++ b/.agent/workflows/setup-flutter.md @@ -0,0 +1,181 @@ +--- +description: How to set up Flutter development environment on Windows +--- + +# Flutter Setup Workflow for Windows + +## 1. Install Flutter SDK + +### Option A: Using Git (Recommended) +```powershell +# Navigate to a directory where you want to install Flutter (e.g., C:\src) +cd C:\ +mkdir src +cd src + +# Clone the Flutter repository +git clone https://github.com/flutter/flutter.git -b stable + +# Add Flutter to your PATH +# Add C:\src\flutter\bin to your system PATH environment variable +``` + +### Option B: Download ZIP +1. Download Flutter SDK from: https://docs.flutter.dev/get-started/install/windows +2. Extract to `C:\src\flutter` +3. Add `C:\src\flutter\bin` to your system PATH + +### Add to PATH (PowerShell as Administrator) +```powershell +# Get current PATH +$currentPath = [Environment]::GetEnvironmentVariable("Path", "User") + +# Add Flutter to PATH (adjust path if you installed elsewhere) +[Environment]::SetEnvironmentVariable( + "Path", + "$currentPath;C:\src\flutter\bin", + "User" +) +``` + +## 2. Verify Flutter Installation + +// turbo +```powershell +# Restart your terminal, then run: +flutter --version +``` + +// turbo +```powershell +# Check for any missing dependencies +flutter doctor +``` + +## 3. Install Required Dependencies + +Based on `flutter doctor` output, you may need to install: + +### Visual Studio (for Windows desktop development) +- Download Visual Studio 2022 Community: https://visualstudio.microsoft.com/downloads/ +- During installation, select "Desktop development with C++" + +### Android Studio (for Android development) +- Download from: https://developer.android.com/studio +- Install Android SDK and Android SDK Command-line Tools + +### Chrome (for web development) +- Already installed on most systems + +## 4. Accept Android Licenses (if developing for Android) + +```powershell +flutter doctor --android-licenses +``` + +## 5. Set Up InnerPod Project + +// turbo +```powershell +# Navigate to the innerpod directory +cd c:\Desktop\innerpod + +# Get all Flutter dependencies +flutter pub get +``` + +## 6. Verify Project Setup + +// turbo +```powershell +# Check for any issues +flutter doctor -v + +# List available devices +flutter devices +``` + +## 7. Build and Run the App + +### For Windows Desktop: +// turbo +```powershell +flutter run -d windows +``` + +### For Web: +```powershell +flutter run -d chrome +``` + +### For Android (with device connected or emulator running): +```powershell +flutter run -d android +``` + +## 8. Build Release Version + +### Windows: +```powershell +flutter build windows +``` + +### Web: +```powershell +flutter build web +``` + +### Android: +```powershell +flutter build apk +``` + +## Troubleshooting + +### Issue: Flutter command not found +- Solution: Restart your terminal after adding Flutter to PATH +- Or manually add to current session: `$env:Path += ";C:\src\flutter\bin"` + +### Issue: Missing Visual Studio +- Solution: Install Visual Studio 2022 with C++ desktop development workload + +### Issue: Android licenses not accepted +- Solution: Run `flutter doctor --android-licenses` and accept all + +### Issue: Pub get fails +- Solution: Check internet connection, try `flutter pub cache repair` + +## Next Steps + +Once Flutter is set up: +1. Fork the innerpod repository on GitHub +2. Clone your fork locally +3. Create a new branch for your changes +4. Make your modifications +5. Test thoroughly +6. Submit a pull request + +## Useful Commands + +```powershell +# Update Flutter SDK +flutter upgrade + +# Clean build artifacts +flutter clean + +# Analyze code for issues +flutter analyze + +# Run tests +flutter test + +# Format code +dart format . + +# Check for outdated packages +flutter pub outdated + +# Upgrade packages +flutter pub upgrade +``` diff --git a/.lycheeignore b/.lycheeignore index fc25087..58bd48b 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -42,6 +42,10 @@ https://.*\.tile-cyclosm\.openstreetmap\.fr/.* web/** +# Placeholder URLs in documentation + +https://github.com/YOUR_USERNAME/innerpod.git + # Licenses. Note that gnu.org is throttled so do not check it. https://www.gnu.org/licenses/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4079b0b..333a6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Here we record the basic changes made to the InnerPod app. + Package for snap release [1.7.5 20251004 gjw] + For GUIDED concat audio then include in app [1.7.4 20250218 gjw] + Review audio. Add 5 minutes option. [1.7.3 20241114 gjw] -+ Updated Tibetan bell from freesound.com [1.7.2 20241101 gjw] ++ Updated Tibetan bell from freesound.org [1.7.2 20241101 gjw] + Use markdown for About with active url links [1.7.1 20241101 gjw] + Move to mp3 rather than ogg for wider OS support [1.7.0 20241025 gjw] diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..fca7ced --- /dev/null +++ b/DEVELOPMENT_GUIDE.md @@ -0,0 +1,461 @@ +# InnerPod Development Guide + +## 📚 Table of Contents + +1. [Project Overview](#project-overview) +2. [Session Recording Architecture](#session-recording-architecture) +3. [Solid Pod Integration](#solid-pod-integration) +4. [Development Setup](#development-setup) +5. [Key Features](#key-features) +6. [Code Structure](#code-structure) +7. [Testing](#testing) +8. [Contributing](#contributing) + +## Project Overview + +InnerPod is a meditation timer app built with Flutter that: + +- Provides guided and unguided meditation sessions +- Records session data to encrypted Solid Pods +- Displays session history +- Works offline (Pod connection is optional) + +**Tech Stack:** + +- **Framework:** Flutter 3.2.5+ +- **Language:** Dart +- **Storage:** Solid Pod (encrypted, decentralized) +- **Key Packages:** + - `solidpod: ^0.7.4` - Solid Pod integration + - `circular_countdown_timer: ^0.2.3` - Timer UI + - `audioplayers: ^6.1.0` - Audio playback + - `intl: ^0.20.2` - Date/time formatting + +## Session Recording Architecture + +### How It Works + +```text +┌─────────────────────────────────────────────────────────────┐ +│ User Starts Session │ +│ (Start/Intro/Guided button) │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Timer Widget (_startTime recorded) │ +│ Session in Progress │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Session Completes │ +│ _complete() method is called │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ _saveSession() Method │ +│ 1. Check if user is logged into Pod │ +│ 2. Read existing sessions.ttl from Pod │ +│ 3. Parse TTL data of sessions │ +│ 4. Append new session {start, end} as RDF │ +│ 5. Write back to Pod (encrypted) │ +└────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ History Widget │ +│ - Reads sessions.ttl from Pod │ +│ - Displays in DataTable format │ +│ - Shows: Date, Start Time, End Time │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Data Format + +Sessions are stored in `sessions.ttl` on the user's Solid Pod: + +```ttl +@prefix : <#>. +@prefix xsd: . + +:session_1739097000000 a :Session; + :start "2026-02-09T10:30:00.000Z"^^xsd:dateTime; + :end "2026-02-09T10:50:00.000Z"^^xsd:dateTime. + +:session_1739175300000 a :Session; + :start "2026-02-10T08:15:00.000Z"^^xsd:dateTime; + :end "2026-02-10T08:35:00.000Z"^^xsd:dateTime. +``` + +**Format Details:** + +- ISO 8601 timestamp format +- UTC timezone +- Millisecond precision + +### Key Code Locations + +#### 1. Session Recording (`lib/widgets/timer.dart`) + +```dart +// Lines 229-252: Session saving logic +Future _saveSession() async { + if (_startTime == null) return; + + final endTime = DateTime.now(); + final session = { + 'start': _startTime!.toIso8601String(), + 'end': endTime.toIso8601String(), + }; + + try { + // Read existing sessions from Pod + String? content = await readPod('sessions.ttl'); + + // Append new session using helper + String newContent = addSession(content, session); + + // Write back to Pod + await writePod('sessions.ttl', newContent); + logMessage('Session saved to Pod'); + } catch (e) { + logMessage('Error saving session to Pod: $e'); + } + + _startTime = null; +} +``` + +#### 2. History Display (`lib/widgets/history.dart`) + +```dart +// Lines 31-60: Loading sessions from Pod +Future _loadSessions() async { + setState(() { + _isLoading = true; + }); + + try { + String? content = await readPod('sessions.ttl'); + 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(); + }); + } + } catch (e) { + debugPrint('Error loading sessions: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } +} +``` + +## Solid Pod Integration + +### What is a Solid Pod? + +Solid (Social Linked Data) is a decentralized web platform where: + +- Users own their data +- Data is stored in personal "Pods" (Personal Online Data stores) +- Apps request permission to access data +- Data is encrypted and private by default + +### How InnerPod Uses Solid Pods + +1. **Optional Login** (`lib/main.dart`): + + ```dart + SolidLogin( + title: 'MANAGE YOUR INNER POD', + required: false, // App works without login + child: Home(), + ) + ``` + +2. **Reading Data**: + + ```dart + String? content = await readPod('sessions.ttl'); + ``` + +3. **Writing Data**: + + ```dart + await writePod('sessions.ttl', newContent); + ``` + +### Benefits of Solid Pod Storage + +- ✅ **Privacy:** Data is encrypted and only accessible to the user +- ✅ **Ownership:** Users control their data, not the app developer +- ✅ **Portability:** Data can be accessed by other Solid-compatible apps +- ✅ **Decentralized:** No central server storing user data +- ✅ **Secure:** Uses WebID authentication + +## Development Setup + +### Prerequisites + +1. **Flutter SDK** (3.2.5 or higher) +2. **Dart SDK** (included with Flutter) +3. **Git** +4. **IDE:** VS Code or Android Studio +5. **Platform-specific tools:** + - Windows: Visual Studio 2022 with C++ desktop development + - Android: Android Studio + Android SDK + - Web: Chrome browser + +### Getting Started + +```bash +# 1. Fork the repository on GitHub +# Visit: https://github.com/gjwgit/innerpod + +# 2. Clone your fork +git clone https://github.com/YOUR_USERNAME/innerpod.git +cd innerpod + +# 3. Install dependencies +flutter pub get + +# 4. Check setup +flutter doctor + +# 5. Run the app +flutter run -d windows # or chrome, android, etc. +``` + +## Key Features + +### 1. Timer Widget (`lib/widgets/timer.dart`) + +**Responsibilities:** + +- Countdown timer display +- Session management (start, pause, resume, reset) +- Audio playback (bells, guided meditation) +- Session recording + +**Key Methods:** + +- `_intro()` - Plays intro audio then starts session +- `_guided()` - Plays full guided meditation +- `_complete()` - Called when session ends +- `_saveSession()` - Saves session data to Pod + +### 2. History Widget (`lib/widgets/history.dart`) + +**Responsibilities:** + +- Display past sessions in a table +- Load sessions from Solid Pod +- Refresh functionality + +**Features:** + +- Date formatting (yyyy-MM-dd) +- Time formatting (HH:mm:ss) +- Loading state indicator +- Empty state message +- Pull-to-refresh + +### 3. Home Widget (`lib/home.dart`) + +**Responsibilities:** + +- Navigation between tabs (Home, Instructions, History) +- App bar with version info +- About dialog + +## Code Structure + +```text +innerpod/ +├── lib/ +│ ├── main.dart # App entry point, Solid login +│ ├── home.dart # Main navigation & app bar +│ ├── widgets/ +│ │ ├── timer.dart # Timer & session recording +│ │ ├── history.dart # Session history display +│ │ ├── instructions.dart # Help/instructions +│ │ ├── app_button.dart # Custom button widget +│ │ └── app_circular_countdown_timer.dart +│ ├── constants/ +│ │ ├── colours.dart # Color definitions +│ │ ├── audio.dart # Audio file paths +│ │ └── spacing.dart # Layout constants +│ └── utils/ +│ ├── ding_dong.dart # Bell sound helper +│ ├── log_message.dart # Logging utility +│ └── word_wrap.dart # Text formatting +├── assets/ +│ ├── images/ # App icons & images +│ └── sounds/ # Audio files +├── pubspec.yaml # Dependencies +└── README.md # Documentation +``` + +## Testing + +### Manual Testing Checklist + +#### Session Recording + +- [ ] Start a session without Pod login (should not crash) +- [ ] Start a session with Pod login (should save) +- [ ] Complete multiple sessions (should append to list) +- [ ] Check sessions.json in Pod (should be valid JSON) + +#### History Display + +- [ ] View history without Pod login (should show empty state) +- [ ] View history with sessions (should display table) +- [ ] Refresh history (should reload data) +- [ ] Check date/time formatting (should be readable) + +#### Edge Cases + +- [ ] Network interruption during save +- [ ] Corrupted sessions.json file +- [ ] Very long session (hours) +- [ ] Session started but app closed before completion + +### Running Tests + +```bash +# Run all tests +flutter test + +# Run with coverage +flutter test --coverage + +# Analyze code +flutter analyze +``` + +## Contributing + +### Workflow + +1. **Fork & Clone** + + ```bash + git clone https://github.com/YOUR_USERNAME/innerpod.git + ``` + +2. **Create Branch** + + ```bash + git checkout -b feature/your-feature-name + ``` + +3. **Make Changes** + - Follow existing code style + - Add comments for complex logic + - Update documentation + +4. **Test** + + ```bash + flutter test + flutter analyze + dart format . + ``` + +5. **Commit** + + ```bash + git add . + git commit -m "feat: add session duration calculation" + ``` + +6. **Push & PR** + + ```bash + git push origin feature/your-feature-name + # Create Pull Request on GitHub + ``` + +### Code Style + +- Follow [Dart style guide](https://dart.dev/guides/language/effective-dart/style) +- Use `dart format .` before committing +- Add doc comments for public APIs +- Keep functions focused and small + +### Potential Enhancements + +Here are some ideas for improving the session recording feature: + +1. **Session Duration Display** + - Calculate and display session duration in history + - Show statistics (total time, average session length) + +2. **Session Types** + - Track session type (Start, Intro, Guided) + - Filter history by session type + +3. **Session Notes** + - Allow users to add notes after a session + - Display notes in history + +4. **Data Visualization** + - Charts showing meditation frequency + - Calendar view of sessions + - Streak tracking + +5. **Export Functionality** + - Export sessions to CSV + - Share session data + +6. **Offline Support** + - Queue sessions when offline + - Sync to Pod when connection restored + +7. **Session Reminders** + - Daily meditation reminders + - Customizable notification times + +8. **Enhanced Encryption** + - Additional encryption layer for sensitive notes + - Backup/restore functionality + +## Resources + +### Documentation + +- [Flutter Docs](https://docs.flutter.dev/) +- [Dart Language Tour](https://dart.dev/guides/language/language-tour) +- [Solid Project](https://solidproject.org/) +- [solidpod Package](https://pub.dev/packages/solidpod) + +### InnerPod Specific + +- [GitHub Repository](https://github.com/gjwgit/innerpod) +- [Online Demo](https://innerpod.solidcommunity.au) +- [Changelog](https://github.com/gjwgit/innerpod/blob/dev/CHANGELOG.md) + +### Community + +- [Flutter Community](https://flutter.dev/community) +- [Solid Community](https://forum.solidproject.org/) + +--- + +Happy Coding! 🧘‍♂️ + +For questions or issues, please open an issue on GitHub or contact the maintainers. diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..93fa31d --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,347 @@ +# InnerPod: Getting Started Guide + +## 🎉 Welcome + +This guide will help you get started with contributing to InnerPod, +specifically focusing on the session recording and history features. + +## 📋 What You Need to Know + +### ✅ Good News: Features Already Implemented + +The session recording and history features you mentioned are +**already implemented** in InnerPod! Here's what's working: + +1. **Automatic Session Recording** + - Sessions are automatically saved when completed + - Data includes start time and end time + - Stored encrypted in user's Solid Pod + +2. **History Display** + - View all past sessions in a table + - Shows date, start time, and end time + - Accessible via the History tab in the bottom navigation + +3. **Solid Pod Integration** + - Uses `solidpod` package from pub.dev + - Data is encrypted and private + - Works offline (Pod connection is optional) + +### 📁 Key Files to Understand + +| File | Purpose | Lines of Interest | +| :--- | :--- | :--- | +| `lib/widgets/timer.dart` | Timer & recording | 229-252 (_saveSession) | +| `lib/widgets/history.dart` | Display history | 31-60 (_loadSessions) | +| `lib/main.dart` | Entry & Solid login | 64-78 (SolidLogin) | +| `lib/home.dart` | Navigation | 110-113 (pages list) | + +## 🚀 Quick Start + +### Step 1: Install Flutter + +**Windows Setup:** + +```powershell +# 1. Download Flutter SDK +# Visit: https://docs.flutter.dev/get-started/install + +# 2. Extract to C:\src\flutter + +# 3. Add to PATH (PowerShell as Administrator) +$currentPath = [Environment]::GetEnvironmentVariable("Path", "User") +[Environment]::SetEnvironmentVariable("Path", + "$currentPath;C:\src\flutter\bin", "User") + +# 4. Restart terminal and verify +flutter --version +flutter doctor +``` + +**Detailed instructions:** See `.agent/workflows/setup-flutter.md` + +### Step 2: Set Up the Project + +```powershell +# Navigate to innerpod directory +cd c:\Desktop\innerpod + +# Install dependencies +flutter pub get + +# Check for issues +flutter doctor + +# List available devices +flutter devices +``` + +### Step 3: Run the App + +```powershell +# For Windows desktop +flutter run -d windows + +# For web browser +flutter run -d chrome + +# For Android (with device/emulator) +flutter run -d android +``` + +## 📚 Documentation Created for You + +I've created three comprehensive guides to help you: + +### 1. **DEVELOPMENT_GUIDE.md** + +Complete development documentation covering: + +- Project architecture overview +- Session recording implementation details +- Solid Pod integration explanation +- Code structure and organization +- Testing strategies +- Contribution guidelines + +**When to use:** Understanding how the app works, architecture decisions, +and best practices + +### 2. **SESSION_ENHANCEMENTS.md** + +Enhancement ideas with implementation details: + +- 10 potential improvements (from easy to advanced) +- Code examples for each enhancement +- Effort estimates and priority matrix +- Recommended implementation order + +**When to use:** Planning new features, looking for contribution ideas + +### 3. **.agent/workflows/setup-flutter.md** + +Step-by-step Flutter setup workflow: + +- Installing Flutter SDK on Windows +- Setting up development environment +- Configuring PATH variables +- Verifying installation +- Running the app + +**When to use:** Setting up your development environment + +## 🎯 Suggested Next Steps + +### Option A: Get Familiar with the Codebase + +1. **Set up Flutter** (use `/setup-flutter` workflow) +2. **Run the app** locally +3. **Explore the code:** + - Read `lib/main.dart` to understand app structure + - Study `lib/widgets/timer.dart` to see session recording + - Review `lib/widgets/history.dart` to see data display +4. **Test the features:** + - Create a Solid Pod account (optional) + - Run a meditation session + - View the history tab + - Check the saved data + +### Option B: Start Contributing + +1. **Fork the repository** on GitHub + - Visit: + - Click "Fork" button + +2. **Clone your fork** + + ```bash + git clone https://github.com/YOUR_USERNAME/innerpod.git + cd innerpod + ``` + +3. **Pick a quick win** from SESSION_ENHANCEMENTS.md + - Recommended first task: **Session Duration Display** + - Estimated time: 30 minutes + - High impact, low effort + +4. **Create a branch** + + ```bash + git checkout -b feature/session-duration + ``` + +5. **Implement the feature** + - Follow the code examples in SESSION_ENHANCEMENTS.md + - Test thoroughly + - Commit your changes + +6. **Submit a Pull Request** + - Push to your fork + - Create PR on original repository + - Describe your changes + +## 💡 Quick Reference + +### Running Commands + +```powershell +# Install dependencies +flutter pub get + +# Run app (Windows) +flutter run -d windows + +# Build release +flutter build windows + +# Run tests +flutter test + +# Analyze code +flutter analyze + +# Format code +dart format . +``` + +### Project Structure + +```text +innerpod/ +├── lib/ +│ ├── main.dart # App entry point +│ ├── home.dart # Main navigation +│ ├── widgets/ +│ │ ├── timer.dart # ⭐ Session recording +│ │ └── history.dart # ⭐ Session history +│ ├── constants/ +│ └── utils/ +├── assets/ # Images & sounds +├── pubspec.yaml # Dependencies +└── README.md # Project info +``` + +### Key Packages Used + +```yaml +solidpod: ^0.7.4 # Solid Pod integration +circular_countdown_timer: ^0.2.3 # Timer UI +audioplayers: ^6.1.0 # Audio playback +intl: ^0.20.2 # Date/time formatting +``` + +## 🔍 Understanding Session Recording + +### How It Works (Simplified) + +```text +1. User starts session + ↓ +2. App records start time + ↓ +3. Timer counts down + ↓ +4. Session completes + ↓ +5. App records end time + ↓ +6. Data saved to Pod (if logged in) + { + "start": "2026-02-09T10:30:00.000Z", + "end": "2026-02-09T10:50:00.000Z" + } + ↓ +7. History tab displays all sessions +``` + +### Data Storage + +**Location:** User's Solid Pod +**File:** `sessions.json` +**Format:** JSON array of session objects +**Encryption:** Handled by Solid Pod + +**Example:** + +```json +[ + { + "start": "2026-02-09T10:30:00.000Z", + "end": "2026-02-09T10:50:00.000Z" + }, + { + "start": "2026-02-10T08:15:00.000Z", + "end": "2026-02-10T08:35:00.000Z" + } +] +``` + +## 🎨 Easy First Contributions + +### 1. Add Session Duration (30 min) + +**File:** `lib/widgets/history.dart` +**Change:** Calculate and display session duration +**Difficulty:** 🟢 Beginner + +### 2. Track Session Type (1 hour) + +**File:** `lib/widgets/timer.dart` +**Change:** Add 'type' field (start/intro/guided) +**Difficulty:** 🟢 Beginner + +### 3. Show Statistics (2 hours) + +**File:** `lib/widgets/history.dart` +**Change:** Display total sessions, total time, average +**Difficulty:** 🟡 Intermediate + +**See SESSION_ENHANCEMENTS.md for detailed implementation guides!** + +## 🤝 Getting Help + +### Resources + +- **Flutter Docs:** +- **Dart Language:** +- **Solid Project:** +- **InnerPod GitHub:** + +### Common Issues + +**Q: Flutter command not found** +A: Restart terminal after adding to PATH, or run: + +```powershell +$env:Path += ";C:\src\flutter\bin" +``` + +**Q: Visual Studio required error** +A: Install Visual Studio 2022 with C++ desktop development workload + +**Q: How do I test without a Solid Pod?** +A: App works offline! Just click "SESSION" on login screen + +**Q: Where is session data stored locally?** +A: Only in Solid Pod. Without login, sessions aren't saved (by design) + +## 🎯 Your Mission (If You Choose to Accept) + +1. ✅ Set up Flutter development environment +2. ✅ Run InnerPod locally +3. ✅ Understand session recording architecture +4. ✅ Pick an enhancement from SESSION_ENHANCEMENTS.md +5. ✅ Implement and test +6. ✅ Submit a Pull Request +7. ✅ Celebrate! 🎉 + +## 📞 Contact + +- **GitHub Issues:** +- **Original Author:** Graham Williams +- **Project:** Togaware + +--- + +**Happy coding! May your meditation sessions be peaceful and your code +bug-free! 🧘‍♂️✨** diff --git a/README.md b/README.md index e083240..cfbf4e2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![GitHub License](https://img.shields.io/github/license/gjwgit/innerpod)](https://raw.githubusercontent.com/gjwgit/innerpod/main/LICENSE) [![Flutter Version](https://img.shields.io/badge/dynamic/yaml?url=https://raw.githubusercontent.com/gjwgit/innerpod/master/pubspec.yaml&query=$.version&label=version)](https://github.com/gjwgit/innerpod/blob/dev/CHANGELOG.md) [![Last Updated](https://img.shields.io/github/last-commit/gjwgit/innerpod?label=last%20updated)](https://github.com/gjwgit/innerpod/commits/dev/) -[![GitHub commit activity (dev)](https://img.shields.io/github/commit-activity/w/gjwgit/innerpod/dev)](https://github.com/gjwgit/rattle/commits/dev/) +[![GitHub commit activity (dev)](https://img.shields.io/github/commit-activity/w/gjwgit/innerpod/dev)](https://github.com/gjwgit/innerpod/commits/dev/) [![GitHub Issues](https://img.shields.io/github/issues/gjwgit/innerpod)](https://github.com/gjwgit/innerpod/issues) [![Google Play](https://img.shields.io/badge/Google%20Play-Available-green?logo=google-play)](https://play.google.com/store/apps/details?id=com.togaware.innerpod) @@ -15,7 +15,7 @@ [![Get it from the Snap Store](https://snapcraft.io/en/light/install.svg)](https://snapcraft.io/innerpod) InnerPod is an app to guide and time your regular mediation. The app -was developed by [Togaware](https://togaware.com.au) and written by +was developed by [Togaware](https://togaware.com) and written by [Graham Williams](https://togaware.com/Graham.Williams.html). If you appreciate the app then please show some ❤️ and star the GitHub @@ -116,7 +116,7 @@ the [Android app](https://play.google.com/store/apps/details?id=com.togaware.innerpod). For more information on the Solid project visit the [Solid Project -AU](https://solidporject.au) site. +AU](https://solidproject.org) site. ## Acknowledgements @@ -134,14 +134,15 @@ The instructions for meditating by John Main are from [WCCM](https://wccm.org). The bell is Tibetan bowl_left hit.wav by -[dersinnsspace](https://freesound.org/s/417117/). License: Creative +[dersinnsspace](https://freesound.org/people/dersinnsspace/sounds/417117/). +License: Creative Commons 0 ## Contributing Feel free to pickup tasks from the list in Issues and so create a fork to work on the issue to then submit a pull request. Or else contact -innerpod@togaware.com to volunteer to work directly on the project + to volunteer to work directly on the project under out guidance. [![Flutter](https://img.shields.io/badge/Flutter-%2302569B.svg?style=for-the-badge&logo=Flutter&logoColor=white)](https://flutter.dev) diff --git a/SESSION_ENHANCEMENTS.md b/SESSION_ENHANCEMENTS.md new file mode 100644 index 0000000..1a2059e --- /dev/null +++ b/SESSION_ENHANCEMENTS.md @@ -0,0 +1,619 @@ +# Session Recording Enhancement Ideas + +This document outlines potential enhancements to the InnerPod session +recording and history features. + +## 🎯 Quick Wins (Easy to Implement) + +### 1. Session Duration Display + +**Current State:** Only start and end times are shown +**Enhancement:** Calculate and display session duration + +**Implementation:** + +```dart +// In lib/widgets/history.dart +_sessions = jsonList.map((item) { + final start = DateTime.parse(item['start']); + final end = DateTime.parse(item['end']); + final duration = end.difference(start); + + return { + 'date': DateFormat('yyyy-MM-dd').format(start), + 'start': DateFormat('HH:mm:ss').format(start), + 'end': DateFormat('HH:mm:ss').format(end), + 'duration': '${duration.inMinutes} min ${duration.inSeconds % 60} sec', + }; +}).toList(); + +// Add duration column to DataTable +DataColumn(label: Text('Duration')), +// ... +DataCell(Text(session['duration']!)), +``` + +**Effort:** 🟢 Low (30 minutes) + +--- + +### 2. Session Type Tracking + +**Current State:** No distinction between Start/Intro/Guided sessions +**Enhancement:** Track and display session type + +**Implementation:** + +```dart +// In lib/widgets/timer.dart, modify _saveSession() +final session = { + 'start': _startTime!.toIso8601String(), + 'end': endTime.toIso8601String(), + 'type': _sessionType, // Add this field +}; + +// Add state variable +String _sessionType = 'start'; // or 'intro', 'guided' + +// Update in each button handler +onPressed: () { + _sessionType = 'intro'; + _intro(); +} +``` + +**Effort:** 🟢 Low (1 hour) + +--- + +### 3. Total Statistics Display + +**Current State:** No summary statistics +**Enhancement:** Show total meditation time, session count, average duration + +**Implementation:** + +```dart +// In lib/widgets/history.dart +Widget _buildStatistics() { + if (_sessions.isEmpty) return SizedBox.shrink(); + + int totalMinutes = 0; + for (var session in _sessions) { + final start = DateTime.parse(session['start']); + final end = DateTime.parse(session['end']); + totalMinutes += end.difference(start).inMinutes; + } + + final avgMinutes = totalMinutes ~/ _sessions.length; + + return Card( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + Text('Total Sessions: ${_sessions.length}'), + Text('Total Time: ${totalMinutes ~/ 60}h ${totalMinutes % 60}m'), + Text('Average Duration: $avgMinutes minutes'), + ], + ), + ), + ); +} +``` + +**Effort:** 🟡 Medium (2 hours) + +--- + +## 🚀 Medium Enhancements + +### 4. Session Notes + +**Enhancement:** Allow users to add notes after completing a session + +**Data Format:** + +```json +{ + "start": "2026-02-09T10:30:00.000Z", + "end": "2026-02-09T10:50:00.000Z", + "type": "guided", + "notes": "Felt very peaceful today. Mind was calm." +} +``` + +**UI Flow:** + +1. After session completes, show optional notes dialog +2. User can add/skip notes +3. Notes stored with session data +4. Display notes in history (expandable row or detail view) + +**Implementation Locations:** + +- `lib/widgets/timer.dart` - Add notes dialog after _complete() +- `lib/widgets/history.dart` - Display notes in expandable rows +- Consider creating `lib/widgets/session_notes_dialog.dart` + +**Effort:** 🟡 Medium (4-6 hours) + +--- + +### 5. Calendar View + +**Enhancement:** Show sessions on a calendar with visual indicators + +**Features:** + +- Calendar grid showing current month +- Days with sessions highlighted +- Tap day to see sessions for that date +- Color coding by session type + +**Packages to Use:** + +- `table_calendar: ^3.0.9` + +**Implementation:** + +```dart +// New file: lib/widgets/calendar_view.dart +import 'package:table_calendar/table_calendar.dart'; + +class CalendarView extends StatefulWidget { + // Calendar implementation +} +``` + +**Effort:** 🟡 Medium (6-8 hours) + +--- + +### 6. Export to CSV + +**Enhancement:** Export session history to CSV file + +**Implementation:** + +```dart +// In lib/widgets/history.dart +import 'package:csv/csv.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; + +Future _exportToCSV() async { + List> rows = [ + ['Date', 'Start Time', 'End Time', 'Duration', 'Type'], + ]; + + for (var session in _sessions) { + rows.add([ + session['date'], + session['start'], + session['end'], + session['duration'], + session['type'] ?? 'start', + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + + final directory = await getApplicationDocumentsDirectory(); + final path = '${directory.path}/innerpod_sessions.csv'; + final file = File(path); + await file.writeAsString(csv); + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Exported to $path')), + ); +} +``` + +**Dependencies to Add:** + +```yaml +dependencies: + csv: ^6.0.0 + path_provider: ^2.1.0 +``` + +**Effort:** 🟡 Medium (3-4 hours) + +--- + +## 🎨 Advanced Features + +### 7. Data Visualization Charts + +**Enhancement:** Visual charts showing meditation patterns + +**Chart Types:** + +- Line chart: Sessions per week/month +- Bar chart: Session duration over time +- Pie chart: Session type distribution +- Heatmap: Meditation frequency calendar + +**Packages:** + +- `fl_chart: ^0.66.0` (recommended) +- `charts_flutter: ^0.12.0` + +**Example:** + +```dart +// lib/widgets/statistics_chart.dart +import 'package:fl_chart/fl_chart.dart'; + +class SessionsLineChart extends StatelessWidget { + final List> sessions; + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + // Chart configuration + ), + ); + } +} +``` + +**Effort:** 🔴 High (10-15 hours) + +--- + +### 8. Streak Tracking + +**Enhancement:** Track consecutive days of meditation + +**Features:** + +- Current streak counter +- Longest streak record +- Visual streak calendar +- Streak milestones (7 days, 30 days, 100 days) +- Motivational messages + +**Implementation:** + +```dart +// lib/utils/streak_calculator.dart +class StreakCalculator { + static int calculateCurrentStreak(List> sessions) { + if (sessions.isEmpty) return 0; + + // Sort sessions by date + sessions.sort((a, b) => + DateTime.parse(b['start']).compareTo(DateTime.parse(a['start'])) + ); + + int streak = 0; + DateTime lastDate = DateTime.now(); + + for (var session in sessions) { + final sessionDate = DateTime.parse(session['start']); + final daysDiff = lastDate.difference(sessionDate).inDays; + + if (daysDiff <= 1) { + streak++; + lastDate = sessionDate; + } else { + break; + } + } + + return streak; + } + + static int calculateLongestStreak(List> sessions) { + // Implementation for longest streak + } +} +``` + +**Effort:** 🔴 High (8-12 hours) + +--- + +### 9. Offline Queue & Sync + +**Enhancement:** Queue sessions when offline, sync when connection restored + +**Architecture:** + +```text +┌─────────────────────────────────────┐ +│ Session Completes │ +└──────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ Check Internet Connection │ +└──────────────┬──────────────────────┘ + │ + ┌───────┴────────┐ + ▼ ▼ +┌─────────────┐ ┌─────────────────┐ +│ Online │ │ Offline │ +│ Save to │ │ Save to Local │ +│ Pod │ │ Queue │ +└─────────────┘ └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │ Connection │ + │ Restored │ + └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │ Sync Queue │ + │ to Pod │ + └─────────────────┘ +``` + +**Implementation:** + +```dart +// lib/services/session_sync_service.dart +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SessionSyncService { + static const String _queueKey = 'session_queue'; + + static Future saveSession(Map session) async { + final connectivity = await Connectivity().checkConnectivity(); + + if (connectivity != ConnectivityResult.none) { + // Online: Save directly to Pod + await _saveToPod(session); + } else { + // Offline: Add to queue + await _addToQueue(session); + } + } + + static Future syncQueue() async { + final prefs = await SharedPreferences.getInstance(); + final queueJson = prefs.getString(_queueKey); + + if (queueJson == null) return; + + List queue = jsonDecode(queueJson); + + for (var session in queue) { + try { + await _saveToPod(session); + } catch (e) { + debugPrint('Failed to sync session: $e'); + return; // Stop syncing if one fails + } + } + + // Clear queue after successful sync + await prefs.remove(_queueKey); + } + + static Future _saveToPod(Map session) async { + String? content = await readPod('sessions.json'); + List sessions = []; + if (content != null && content.isNotEmpty) { + sessions = jsonDecode(content); + } + sessions.add(session); + await writePod('sessions.json', jsonEncode(sessions)); + } + + static Future _addToQueue(Map session) async { + final prefs = await SharedPreferences.getInstance(); + final queueJson = prefs.getString(_queueKey); + + List queue = []; + if (queueJson != null) { + queue = jsonDecode(queueJson); + } + + queue.add(session); + await prefs.setString(_queueKey, jsonEncode(queue)); + } +} +``` + +**Dependencies:** + +```yaml +dependencies: + connectivity_plus: ^5.0.0 + shared_preferences: ^2.2.0 +``` + +**Effort:** 🔴 High (12-16 hours) + +--- + +### 10. Session Reminders + +**Enhancement:** Daily notifications to remind users to meditate + +**Features:** + +- Customizable reminder time +- Multiple reminders per day +- Smart reminders (skip if already meditated) +- Motivational quotes in notifications + +**Implementation:** + +```dart +// lib/services/notification_service.dart +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class NotificationService { + static final FlutterLocalNotificationsPlugin _notifications = + FlutterLocalNotificationsPlugin(); + + static Future initialize() async { + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings(); + + const settings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await _notifications.initialize(settings); + } + + static Future scheduleDailyReminder({ + required int hour, + required int minute, + }) async { + await _notifications.zonedSchedule( + 0, + 'Time to Meditate', + 'Take a moment for your inner peace 🧘', + _nextInstanceOfTime(hour, minute), + const NotificationDetails( + android: AndroidNotificationDetails( + 'meditation_reminder', + 'Meditation Reminders', + channelDescription: 'Daily meditation reminders', + importance: Importance.high, + ), + ), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: + UILocalNotificationDateInterpretation.absoluteTime, + matchDateTimeComponents: DateTimeComponents.time, + ); + } + + static tz.TZDateTime _nextInstanceOfTime(int hour, int minute) { + final now = tz.TZDateTime.now(tz.local); + var scheduledDate = tz.TZDateTime( + tz.local, + now.year, + now.month, + now.day, + hour, + minute, + ); + + if (scheduledDate.isBefore(now)) { + scheduledDate = scheduledDate.add(const Duration(days: 1)); + } + + return scheduledDate; + } +} +``` + +**Dependencies:** + +```yaml +dependencies: + flutter_local_notifications: ^16.0.0 + timezone: ^0.9.0 +``` + +**Effort:** 🔴 High (10-14 hours) + +--- + +## 📊 Implementation Priority Matrix + +| Feature | Impact | Effort | Priority | +| :--- | :--- | :--- | :--- | +| Session Duration | High | Low | ⭐⭐⭐⭐⭐ | +| Session Type | High | Low | ⭐⭐⭐⭐⭐ | +| Total Statistics | High | Medium | ⭐⭐⭐⭐ | +| Session Notes | Medium | Medium | ⭐⭐⭐ | +| Export CSV | Medium | Medium | ⭐⭐⭐ | +| Calendar View | High | Medium | ⭐⭐⭐⭐ | +| Charts | Medium | High | ⭐⭐ | +| Streak Tracking | High | High | ⭐⭐⭐⭐ | +| Offline Sync | High | High | ⭐⭐⭐⭐ | +| Reminders | Medium | High | ⭐⭐⭐ | + +## 🎯 Recommended Implementation Order + +### Phase 1: Quick Wins (Week 1) + +1. Session Duration Display +2. Session Type Tracking +3. Total Statistics Display + +### Phase 2: Enhanced UX (Week 2-3) + +1. Session Notes +2. Export to CSV +3. Calendar View + +### Phase 3: Advanced Features (Week 4-6) + +1. Streak Tracking +2. Offline Queue & Sync +3. Data Visualization Charts +4. Session Reminders + +--- + +## 🛠️ Development Tips + +### Testing Session Recording + +```dart +// Create test sessions programmatically +Future _createTestSessions() async { + final testSessions = [ + { + 'start': DateTime.now().subtract(Duration(days: 5)).toIso8601String(), + 'end': DateTime.now().subtract(Duration(days: 5, minutes: -20)).toIso8601String(), + 'type': 'start', + }, + { + 'start': DateTime.now().subtract(Duration(days: 4)).toIso8601String(), + 'end': DateTime.now().subtract(Duration(days: 4, minutes: -25)).toIso8601String(), + 'type': 'guided', + }, + // Add more test sessions + ]; + + await writePod('sessions.json', jsonEncode(testSessions)); +} +``` + +### Debugging Pod Storage + +```dart +// Read and print current sessions +Future _debugSessions() async { + String? content = await readPod('sessions.json'); + debugPrint('Current sessions: $content'); +} +``` + +### Performance Considerations + +- Cache session data locally to reduce Pod reads +- Implement pagination for large session lists +- Use `ListView.builder` for efficient rendering +- Consider lazy loading for charts + +--- + +## 📝 Notes + +- Maintain backward compatibility with existing session data +- Consider data migration strategies when changing session format +- Test thoroughly with and without Pod connection +- Ensure offline functionality remains robust +- Follow existing code style and patterns + +--- + +**Questions or suggestions?** Open an issue on GitHub! diff --git a/lib/home.dart b/lib/home.dart index d0310bf..d6094e2 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -35,6 +35,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:innerpod/constants/colours.dart'; import 'package:innerpod/utils/word_wrap.dart'; +import 'package:innerpod/widgets/history.dart'; import 'package:innerpod/widgets/instructions.dart'; import 'package:innerpod/widgets/timer.dart'; @@ -108,14 +109,7 @@ class HomeState extends State with SingleTickerProviderStateMixin { final List _pages = [ const Timer(), const Instructions(), - const Column( - children: [ - Gap(100), - Text('Coming Soon'), - Gap(100), - Icon(Icons.chat, size: 150), - ], - ), + const History(), ]; @override diff --git a/lib/main.dart b/lib/main.dart index 15e561f..771e145 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,8 @@ library; import 'package:flutter/material.dart'; +import 'package:solidui/solidui.dart'; + import 'package:innerpod/home.dart'; //import 'package:innerpod/timer.dart'; @@ -58,29 +60,20 @@ class InnerPod extends StatelessWidget { /// at that time. The login token and the security key are (optionally) /// cached so that the login information is not required every time. - // TODO 20240708 gjw COMMENTED OUT FOR NOW BUT INTEND TO USE. - - // 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.', - // ), - // // registerButtonStyle: registerButtonStyle( - // // text: 'REG', - // // ), - // link: 'https://github.com/gjwgit/innerpod/blob/dev/README.md', - // child: Home(), - // ); - - // OR - - return const Home(); + 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(), + ); } } diff --git a/lib/utils/session_logic.dart b/lib/utils/session_logic.dart new file mode 100644 index 0000000..4e64b8c --- /dev/null +++ b/lib/utils/session_logic.dart @@ -0,0 +1,104 @@ +/// Session logic for InnerPod. +// +// Time-stamp: <2026-02-10 16:40:00 Amogh Hosamane> +// +/// 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 +// 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: Amogh Hosamane + +library; + +const String _prefixes = ''' +@prefix : <#>. +@prefix xsd: . +'''; + +/// Parses a TTL string containing session data into a list of maps. +/// Returns a list of sessions, where each session is a map with 'start' and 'end' keys. +List> parseSessions(String? content) { + if (content == null || content.isEmpty) { + return []; + } + + final List> sessions = []; + + // RegExp to match a session block. + // It looks for a block starting with :session_ and ending with a literal dot + // that is followed by whitespace or end of string. + // The dot in timestamps (e.g., .000Z) is NOT followed by whitespace, so this distinguishes the terminator. + final RegExp sessionBlockRegExp = + RegExp(r':session_\d+.*?\.(?:\s+|$)', dotAll: true); + + // RegExp to extract start and end times within a block + final RegExp startRegExp = RegExp(r':start "(.*?)"\^\^xsd:dateTime'); + final RegExp endRegExp = RegExp(r':end "(.*?)"\^\^xsd:dateTime'); + + 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); + + if (startMatch != null && endMatch != null) { + sessions.add({ + 'start': startMatch.group(1)!, + 'end': endMatch.group(1)!, + }); + } + } + + // Sort by start time descending (newest first) + sessions.sort((a, b) => b['start']!.compareTo(a['start']!)); + + return sessions; +} + +/// Adds a new session to the existing TTL content. +/// If currentContent is null or empty, initializes with prefixes. +/// Returns the updated TTL content string. +String addSession(String? currentContent, Map newSession) { + String content = currentContent ?? ''; + + // key fix: trim() handles invisible whitespace that might make "empty" check false + if (content.trim().isEmpty) { + content = _prefixes; + } else if (!content.contains('@prefix')) { + // If somehow content exists but no prefixes (legacy/corrupt), add them + content = '$_prefixes\n$content'; + } + + final String start = newSession['start']; + final String end = newSession['end']; + // Use timestamp as unique ID + final String id = DateTime.parse(start).millisecondsSinceEpoch.toString(); + + // Add newline before new entry if needed + final String separator = content.endsWith('\n') ? '' : '\n'; + + final String newEntry = ''' +$separator +:session_$id a :Session; + :start "$start"^^xsd:dateTime; + :end "$end"^^xsd:dateTime. +'''; + + return content + newEntry; +} diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart new file mode 100644 index 0000000..18ad894 --- /dev/null +++ b/lib/widgets/history.dart @@ -0,0 +1,108 @@ +/// A table of past sessions logged to the user's Solid Pod. +// +// Time-stamp: <2026-02-09 16:45:00 Amogh Hosamane> +// +/// Copyright (C) 2024-2026, Togaware Pty Ltd +/// +/// Licensed under the GNU General Public License, Version 3 (the "License"); +library; + +import 'package:flutter/material.dart'; + +import 'package:intl/intl.dart'; +import 'package:solidpod/solidpod.dart'; + +import 'package:innerpod/utils/session_logic.dart'; + +class History extends StatefulWidget { + const History({super.key}); + + @override + State createState() => _HistoryState(); +} + +class _HistoryState extends State { + List> _sessions = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadSessions(); + } + + Future _loadSessions() async { + 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(); + }); + } + } catch (e) { + debugPrint('Error loading sessions: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Session History'), + automaticallyImplyLeading: false, // Don't show back button + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _loadSessions, + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _sessions.isEmpty + ? const Center(child: Text('No sessions recorded yet.')) + : Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: SizedBox( + width: double.infinity, + child: DataTable( + columns: const [ + DataColumn(label: Text('Date')), + DataColumn(label: Text('Start')), + DataColumn(label: Text('End')), + ], + rows: _sessions.map((session) { + return DataRow( + cells: [ + DataCell(Text(session['date']!)), + DataCell(Text(session['start']!)), + DataCell(Text(session['end']!)), + ], + ); + }).toList(), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/timer.dart b/lib/widgets/timer.dart index 1ea774b..486f17e 100644 --- a/lib/widgets/timer.dart +++ b/lib/widgets/timer.dart @@ -29,12 +29,14 @@ import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:circular_countdown_timer/circular_countdown_timer.dart'; +import 'package:solidpod/solidpod.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:innerpod/constants/audio.dart'; import 'package:innerpod/constants/spacing.dart'; import 'package:innerpod/utils/ding_dong.dart'; import 'package:innerpod/utils/log_message.dart'; +import 'package:innerpod/utils/session_logic.dart'; import 'package:innerpod/widgets/app_button.dart'; import 'package:innerpod/widgets/app_circular_countdown_timer.dart'; @@ -76,6 +78,10 @@ class TimerState extends State { var _audioDuration = Duration.zero; + // Track the start time of a session. + + DateTime? _startTime; + //////////////////////////////////////////////////////////////////////// // CONSTANTS //////////////////////////////////////////////////////////////////////// @@ -125,6 +131,7 @@ class TimerState extends State { _reset(); _stopSleep(); _isGuided = false; + _startTime = DateTime.now(); // Good to wait a second before starting the audio after tapping the button, // otherwise it feels rushed. @@ -167,6 +174,7 @@ class TimerState extends State { _reset(); _stopSleep(); _isGuided = true; + _startTime = DateTime.now(); // Good to wait a second before starting the audio after tapping the button, // otherwise it feels rushed. @@ -215,6 +223,28 @@ class TimerState extends State { _reset(); _allowSleep(); + await _saveSession(); + } + + Future _saveSession() async { + if (_startTime == null) return; + + final endTime = DateTime.now(); + final session = { + 'start': _startTime!.toIso8601String(), + 'end': endTime.toIso8601String(), + }; + + try { + String? content = await readPod('sessions.ttl'); + String newContent = addSession(content, session); + await writePod('sessions.ttl', newContent); + logMessage('Session saved to Pod'); + } catch (e) { + logMessage('Error saving session to Pod: $e'); + } + + _startTime = null; } //////////////////////////////////////////////////////////////////////// @@ -257,6 +287,7 @@ minutes, beginning and ending with three chimes. dingDong(_player); _controller.restart(); _stopSleep(); + _startTime = DateTime.now(); }, fontWeight: FontWeight.bold, backgroundColor: Colors.lightGreenAccent.shade100, diff --git a/pubspec.yaml b/pubspec.yaml index e26c6be..0612b0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,23 +27,19 @@ dependencies: intl: ^0.20.2 markdown_tooltip: ^0.0.7 package_info_plus: ^9.0.0 - solidpod: ^0.10.5 + solidpod: ^0.10.1 + solidui: any url_launcher: ^6.3.0 wakelock_plus: ^1.1.4 -dependency_overrides: - solidpod: - git: - url: https://github.com/anusii/solidpod.git - ref: dev - solidui: - git: - url: https://github.com/anusii/solidui.git - ref: dev + dev_dependencies: flutter_launcher_icons: ^0.14.4 - flutter_lints: ^6.0.0 + flutter_lints: any + ubuntu_lints: any + flutter_test: + sdk: flutter flutter: assets: diff --git a/test/session_logic_test.dart b/test/session_logic_test.dart new file mode 100644 index 0000000..37f5209 --- /dev/null +++ b/test/session_logic_test.dart @@ -0,0 +1,110 @@ +/// Tests for session logic. +// +// Time-stamp: <2026-02-10 16:45:00 Amogh Hosamane> +// +/// 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 +// 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: Amogh Hosamane + +library; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:innerpod/utils/session_logic.dart'; + +void main() { + group('Session Logic TTL Tests', () { + test('parseSessions returns empty list on null content', () { + expect( + parseSessions(null), + isEmpty, + ); + }); + + test('parseSessions returns empty list on empty content', () { + expect( + parseSessions(''), + isEmpty, + ); + }); + + test('parseSessions parses a session correctly', () { + final ttl = ''' +:session_123456789 a :Session; + :start "2024-01-01T10:00:00.000Z"^^xsd:dateTime; + :end "2024-01-01T10:20:00.000Z"^^xsd:dateTime. +'''; + final sessions = parseSessions(ttl); + expect( + sessions.length, + 1, + ); + expect( + sessions.first['start'], + '2024-01-01T10:00:00.000Z', + ); + expect( + sessions.first['end'], + '2024-01-01T10:20:00.000Z', + ); + }); + + test('addSession adds session to empty content with prefixes', () { + final newSession = { + 'start': '2024-01-01T10:00:00.000Z', + 'end': '2024-01-01T10:20:00.000Z', + }; + final result = addSession(null, newSession); + + expect(result.contains('@prefix : <#>.'), isTrue); + expect(result.contains('@prefix xsd:'), isTrue); + expect( + result.contains(':start "2024-01-01T10:00:00.000Z"^^xsd:dateTime'), + isTrue, + ); + }); + + test('addSession appends session to existing content', () { + final existing = ''' +@prefix : <#>. +@prefix xsd: . + +:session_123456789 a :Session; + :start "2023-01-01T00:00:00.000Z"^^xsd:dateTime; + :end "2023-01-01T00:20:00.000Z"^^xsd:dateTime. +'''; + final newSession = { + 'start': '2024-01-01T12:00:00.000Z', + 'end': '2024-01-01T12:20:00.000Z', + }; + final result = addSession(existing, newSession); + + final parsed = parseSessions(result); + expect( + parsed.length, + 2, + ); + expect( + parsed.first['start'], + '2024-01-01T12:00:00.000Z', + ); // Newest first + }); + }); +} diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee30..0270d2e 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -4,7 +4,7 @@ #include "flutter/generated_plugin_registrant.h" -FlutterWindow::FlutterWindow(const flutter::DartProject& project) +FlutterWindow::FlutterWindow(const flutter::DartProject &project) : project_(project) {} FlutterWindow::~FlutterWindow() {} @@ -27,14 +27,7 @@ bool FlutterWindow::OnCreate() { RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); return true; } @@ -59,12 +52,12 @@ FlutterWindow::MessageHandler(HWND hwnd, UINT const message, if (result) { return *result; } - } - switch (message) { + switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; + } } return Win32Window::MessageHandler(hwnd, message, wparam, lparam);