diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/lib/utils/session_logic.dart b/lib/utils/session_logic.dart index 5cfd531..199334d 100644 --- a/lib/utils/session_logic.dart +++ b/lib/utils/session_logic.dart @@ -121,7 +121,7 @@ String serializeSessions(List> sessions) { } /// Adds a new session to the existing TTL content. -/// If currentContent is null or empty, initializes with prefixes. +/// If currentContent is null or empty, initialises with prefixes. /// Returns the updated TTL content string. String addSession(String? currentContent, Map newSession) { List> sessions = parseSessions(currentContent); diff --git a/lib/widgets/history.dart b/lib/widgets/history.dart index 145f2a3..bdc502e 100644 --- a/lib/widgets/history.dart +++ b/lib/widgets/history.dart @@ -30,17 +30,17 @@ library; import 'package:flutter/material.dart'; -import 'package:markdown_tooltip/markdown_tooltip.dart'; import 'package:solidpod/solidpod.dart'; import 'package:solidui/solidui.dart'; import 'package:innerpod/constants/colours.dart' as colours; -import 'package:innerpod/constants/colours.dart'; import 'package:innerpod/utils/local_session_store.dart'; import 'package:innerpod/utils/session_logic.dart'; import 'package:innerpod/widgets/edit_session_dialog.dart'; +import 'package:innerpod/widgets/history_actions.dart'; import 'package:innerpod/widgets/history_backup.dart'; import 'package:innerpod/widgets/history_format.dart'; +import 'package:innerpod/widgets/history_pod.dart'; import 'package:innerpod/widgets/history_stats.dart'; import 'package:innerpod/widgets/history_tile.dart'; @@ -82,74 +82,76 @@ class _HistoryState extends State { await _loadSessions(); } - /// Parse a date string tolerantly, handling both ISO 8601 - /// (e.g. "2026-05-25T14:30:22.000") and the legacy compact format - /// (e.g. "20260525T143022") that older app versions wrote to the Pod. - /// Promote a locally-stored session to the Pod: write it to sessions.ttl, - /// then remove it from the local store. Triggered by tapping the lock icon. - Future _syncToPod(Map session) async { - if (!_isLoggedIn) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please log in first to save to your Pod.'), - ), - ); - } - return; - } + /// Run [action] with the loading indicator showing, then reload the list + /// unless [reload] is false. + /// + /// Every Pod operation shares the same recovery: if the Pod reports the + /// security key is not yet set, prompt for it and retry once. Other + /// failures are logged and, when [onError] is given, reported to the user. + + Future _guard( + Future Function() action, { + String? onError, + bool reload = true, + }) async { setState(() => _isLoading = true); try { - // Reconstruct the raw session map for addSession. - final raw = { - 'start': session['rawStart'], - 'end': session['rawEnd'], - 'type': session['type'], - // Duration is shown as e.g. "20m"; convert back to seconds. - 'silenceDuration': durationToSeconds(session['duration']), - 'title': session['title'], - 'description': session['description'], - }; - - String content = ''; - try { - content = await readPod('sessions.ttl'); - } on ResourceNotExistException { - content = ''; - } - final newContent = addSession(content, raw); - await writePod('sessions.ttl', newContent, overwrite: true); - - // Remove from local store now that it's on the Pod. - await LocalSessionStore.removeSessionLocal(session['rawStart']!); - - await _loadSessions(); + await action(); + if (reload && mounted) await _loadSessions(); } catch (e) { - if (e.toString().contains('You must first set the security key!')) { + if (isMissingKeyError(e)) { if (mounted) { await getKeyFromUserIfRequired(context, widget); - if (mounted) await _syncToPod(session); + if (mounted) await _guard(action, onError: onError, reload: reload); } return; } - debugPrint('Failed to sync local session to Pod: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not save to Pod. Try again.')), - ); - } + debugPrint('[History] ${onError ?? 'operation failed'}: $e'); + if (onError != null) _toast(onError); } finally { if (mounted) setState(() => _isLoading = false); } } - /// Convert a display duration like "20m" back to seconds for storage. + /// Ask the user to confirm a destructive action, returning true if they do. + + Future _confirm(String title, String message, String action) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message, style: const TextStyle(fontSize: 16)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: colours.error.withValues(alpha: 0.1), + foregroundColor: colours.error, + elevation: 0, + ), + child: Text(action), + ), + ], + ), + ); + return confirmed == true; + } + + /// Show a brief message via a SnackBar. + + void _toast(String message) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } + Future _loadSessions() async { - if (mounted) { - setState(() { - _isLoading = true; - }); - } + if (mounted) setState(() => _isLoading = true); try { // Always load the local (un-synced) store. @@ -159,198 +161,88 @@ class _HistoryState extends State { String? content; if (_isLoggedIn) { try { - content = await readPod('sessions.ttl'); - } on ResourceNotExistException { - debugPrint('sessions.ttl does not exist yet (normal for new users)'); - content = null; + content = await readSessions(); } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - debugPrint( - 'Security key missing - cannot access sessions.ttl. Prompting.', - ); + if (isMissingKeyError(e) && mounted) { + debugPrint('[History] security key missing - prompting.'); + await getKeyFromUserIfRequired(context, widget); if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) { - await _loadSessions(); - return; - } + await _loadSessions(); + return; } } - debugPrint('Error accessing sessions.ttl: $e'); + debugPrint('[History] error accessing $sessionsFile: $e'); content = null; } } - final podRaw = parseSessions(content); - // Merge: Pod sessions (not local) + local sessions (tagged local). final sessions = >[ - ...podRaw.map((item) => sessionToDisplay(item)), + ...parseSessions(content).map(sessionToDisplay), ...localRaw.map((item) => sessionToDisplay(item, local: true)), ]; // Sort newest first by raw start timestamp. sessions.sort((a, b) => b['rawStart']!.compareTo(a['rawStart']!)); - if (mounted) { - setState(() { - _sessions = sessions; - }); - } + if (mounted) setState(() => _sessions = sessions); } catch (e) { - debugPrint('Unexpected error loading sessions: $e'); + debugPrint('[History] unexpected error loading sessions: $e'); } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } + if (mounted) setState(() => _isLoading = false); } } - Future _deleteSession(String rawStart, {bool local = false}) async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Delete Session'), - content: const Text( - 'Are you sure you want to delete this session? This action cannot be undone.', - style: TextStyle(fontSize: 16), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, true), - style: ElevatedButton.styleFrom( - backgroundColor: colours.error.withValues(alpha: 0.1), - foregroundColor: colours.error, - elevation: 0, - ), - child: const Text('Delete'), - ), - ], - ), - ); + /// Promote a locally-stored session to the Pod, then remove the local copy. + /// Triggered by tapping the lock icon on a local session. - if (confirmed == true) { - setState(() => _isLoading = true); - // Local sessions are deleted from the device store, not the Pod. - if (local) { - try { - await LocalSessionStore.removeSessionLocal(rawStart); - await _loadSessions(); - } catch (e) { - debugPrint('Error deleting local session: $e'); - } finally { - if (mounted) setState(() => _isLoading = false); - } - return; - } - try { - final content = await readPod('sessions.ttl'); - final newContent = deleteSession(content, rawStart); - await writePod( - 'sessions.ttl', - newContent, - overwrite: true, - ); - await _loadSessions(); - } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - debugPrint( - 'Security key missing - cannot decrypt sessions.ttl for deletion', - ); - if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) { - await _deleteSession(rawStart); - } - } - return; - } - debugPrint('Error deleting session: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to delete session: $e')), - ); - } - } finally { - if (mounted) setState(() => _isLoading = false); - } + Future _syncToPod(Map session) async { + if (!_isLoggedIn) { + _toast('Please log in first to save to your Pod.'); + return; } + await _guard( + () => podSyncSession(session), + onError: 'Could not save to Pod. Try again.', + ); } - Future _deleteAllSessions() async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Delete All Sessions'), - content: const Text( - 'Are you sure you want to delete ALL sessions? This action cannot be undone.', - style: TextStyle(fontSize: 16), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, true), - style: ElevatedButton.styleFrom( - backgroundColor: colours.error.withValues(alpha: 0.1), - foregroundColor: colours.error, - elevation: 0, - ), - child: const Text('Delete All'), - ), - ], - ), + Future _deleteSession(String rawStart, {bool local = false}) async { + final confirmed = await _confirm( + 'Delete Session', + 'Are you sure you want to delete this session? ' + 'This action cannot be undone.', + 'Delete', + ); + if (!confirmed) return; + + // Local sessions are deleted from the device store, not the Pod. + await _guard( + local + ? () => LocalSessionStore.removeSessionLocal(rawStart) + : () => podDeleteSession(rawStart), + onError: local ? null : 'Failed to delete the session. Try again.', ); - - if (confirmed == true) { - await _performDeleteAll(); - } } - Future _performDeleteAll() async { - setState(() => _isLoading = true); - try { - final newContent = serializeSessions([]); - await writePod( - 'sessions.ttl', - newContent, - overwrite: true, - ); - await _loadSessions(); - } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - debugPrint('Security key missing - ' - 'cannot write sessions.ttl for bulk deletion'); - if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) { - await _performDeleteAll(); - } - } - return; - } - debugPrint('Error deleting all sessions: $e'); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to delete all sessions: $e')), - ); - } - } finally { - if (mounted) setState(() => _isLoading = false); - } + Future _deleteAllSessions() async { + final confirmed = await _confirm( + 'Delete All Sessions', + 'Are you sure you want to delete ALL sessions? ' + 'This action cannot be undone.', + 'Delete All', + ); + if (!confirmed) return; + + await _guard( + podDeleteAllSessions, + onError: 'Failed to delete all sessions. Try again.', + ); } Future _editSession(Map session) async { // Parse current start/end into editable DateTime values. End may be - // missing ("null") on old sessions — default it to the start time. + // missing ("null") on old sessions - default it to the start time. final startDt = parseSessionDate(session['rawStart']!); final rawEnd = session['rawEnd'] ?? 'null'; final endDt = (rawEnd.trim() == 'null' || rawEnd.trim().isEmpty) @@ -365,65 +257,37 @@ class _HistoryState extends State { ); if (result == null) return; // cancelled - setState(() => _isLoading = true); - try { - final content = await readPod('sessions.ttl'); - final newContent = updateSession(content, session['rawStart']!, { + await _guard( + () => podUpdateSession(session['rawStart']!, { 'title': result.title, 'description': result.description, 'start': result.start.toIso8601String(), 'end': result.end.toIso8601String(), - }); - await writePod('sessions.ttl', newContent, overwrite: true); - await _loadSessions(); - } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - debugPrint( - 'Security key missing - cannot decrypt sessions.ttl for update', - ); - if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) await _editSession(session); - } - return; - } - } finally { - if (mounted) setState(() => _isLoading = false); - } + }), + ); } /// Export all sessions to a .ttl backup file, prompting for the location. + Future _exportBackup() async { if (!_isLoggedIn) { _toast('Please log in first to back up your history.'); return; } - setState(() => _isLoading = true); - try { - String content; - try { - content = await readPod('sessions.ttl'); - } on ResourceNotExistException { - content = serializeSessions([]); - } - if (await saveTtlBackup(content)) _toast('History exported.'); - } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) await _exportBackup(); + await _guard( + () async { + if (await saveTtlBackup(await readSessions())) { + _toast('History exported.'); } - return; - } - debugPrint('[History] export failed: $e'); - _toast('Could not export history. Try again.'); - } finally { - if (mounted) setState(() => _isLoading = false); - } + }, + onError: 'Could not export history. Try again.', + reload: false, + ); } /// Import sessions from a .ttl backup file, merging them into the Pod. /// Sessions whose start time already exists are skipped. + Future _importBackup() async { if (!_isLoggedIn) { _toast('Please log in first to restore your history.'); @@ -432,44 +296,16 @@ class _HistoryState extends State { final importedContent = await pickTtlBackup(); if (importedContent == null) return; // cancelled / unreadable - setState(() => _isLoading = true); - try { - String content; - try { - content = await readPod('sessions.ttl'); - } on ResourceNotExistException { - content = serializeSessions([]); - } - - final merged = mergeBackup(content, importedContent); - await writePod('sessions.ttl', merged.content, overwrite: true); - await _loadSessions(); - _toast( - merged.added == 0 - ? 'No new sessions to import.' - : 'Imported ${merged.added} ' - 'session${merged.added == 1 ? '' : 's'}.', - ); - } catch (e) { - if (e.toString().contains('You must first set the security key!')) { - if (mounted) { - await getKeyFromUserIfRequired(context, widget); - if (mounted) await _importBackup(); - } - return; - } - debugPrint('[History] import failed: $e'); - _toast('Could not import history. Try again.'); - } finally { - if (mounted) setState(() => _isLoading = false); - } - } - - /// Show a brief message via a SnackBar. - void _toast(String message) { - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), + await _guard( + () async { + final added = await podImportBackup(importedContent); + _toast( + added == 0 + ? 'No new sessions to import.' + : 'Imported $added session${added == 1 ? '' : 's'}.', + ); + }, + onError: 'Could not import history. Try again.', ); } @@ -477,74 +313,17 @@ class _HistoryState extends State { Widget build(BuildContext context) { return Column( children: [ - // Action row replacing the AppBar actions. - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - MarkdownTooltip( - message: '**Export Backup**\n\n' - 'Save all your session history to a .ttl backup file. ' - 'You will be prompted for where to save it.', - child: IconButton( - icon: const Icon(Icons.file_upload_outlined), - tooltip: 'Export Backup', - onPressed: _exportBackup, - ), - ), - MarkdownTooltip( - message: '**Import Backup**\n\n' - 'Restore sessions from a previously exported .ttl backup ' - 'file. Existing sessions are kept; only new ones are added.', - child: IconButton( - icon: const Icon(Icons.file_download_outlined), - tooltip: 'Import Backup', - onPressed: _importBackup, - ), - ), - if (_sessions.isNotEmpty) - IconButton( - icon: const Icon( - Icons.delete_sweep_outlined, - color: colours.error, - ), - tooltip: 'Delete all sessions', - onPressed: _deleteAllSessions, - ), - IconButton( - icon: const Icon(Icons.refresh), - tooltip: 'Refresh', - onPressed: _loadSessions, - ), - const SizedBox(width: 8), - ], + HistoryActions( + onExport: _exportBackup, + onImport: _importBackup, + onRefresh: _loadSessions, + onDeleteAll: _sessions.isEmpty ? null : _deleteAllSessions, ), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _sessions.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _isLoggedIn ? Icons.history : Icons.lock_outline, - size: 64, - color: historyNoneColor, - ), - const SizedBox(height: 16), - Text( - _isLoggedIn - ? 'No sessions recorded yet.' - : 'No sessions available.\nPlease login to view the session history.', - textAlign: TextAlign.center, - style: TextStyle( - color: historyNoneColor, - fontSize: 16, - ), - ), - ], - ), - ) + ? HistoryEmpty(isLoggedIn: _isLoggedIn) : ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), // +1 for the stats header at index 0. diff --git a/lib/widgets/history_actions.dart b/lib/widgets/history_actions.dart new file mode 100644 index 0000000..18f05e1 --- /dev/null +++ b/lib/widgets/history_actions.dart @@ -0,0 +1,134 @@ +/// HistoryActions — the action row and empty-state placeholder for the +/// InnerPod history, extracted from history.dart to keep that widget within +/// the project line-count limit. +/// +/// 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 + +library; + +import 'package:flutter/material.dart'; + +import 'package:markdown_tooltip/markdown_tooltip.dart'; + +import 'package:innerpod/constants/colours.dart' as colours; +import 'package:innerpod/constants/colours.dart'; + +/// The row of history actions shown in place of the AppBar actions. + +class HistoryActions extends StatelessWidget { + const HistoryActions({ + required this.onExport, + required this.onImport, + required this.onRefresh, + this.onDeleteAll, + super.key, + }); + + final VoidCallback onExport; + final VoidCallback onImport; + final VoidCallback onRefresh; + + /// When null the delete-all button is hidden, as there is nothing to delete. + + final VoidCallback? onDeleteAll; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MarkdownTooltip( + message: '**Export Backup**\n\n' + 'Save all your session history to a .ttl backup file. ' + 'You will be prompted for where to save it.', + child: IconButton( + icon: const Icon(Icons.file_upload_outlined), + tooltip: 'Export Backup', + onPressed: onExport, + ), + ), + MarkdownTooltip( + message: '**Import Backup**\n\n' + 'Restore sessions from a previously exported .ttl backup ' + 'file. Existing sessions are kept; only new ones are added.', + child: IconButton( + icon: const Icon(Icons.file_download_outlined), + tooltip: 'Import Backup', + onPressed: onImport, + ), + ), + if (onDeleteAll != null) + IconButton( + icon: const Icon( + Icons.delete_sweep_outlined, + color: colours.error, + ), + tooltip: 'Delete all sessions', + onPressed: onDeleteAll, + ), + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: onRefresh, + ), + const SizedBox(width: 8), + ], + ); + } +} + +/// The placeholder shown when there are no sessions to list, prompting a +/// logged-out user to log in. + +class HistoryEmpty extends StatelessWidget { + const HistoryEmpty({required this.isLoggedIn, super.key}); + + final bool isLoggedIn; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isLoggedIn ? Icons.history : Icons.lock_outline, + size: 64, + color: historyNoneColor, + ), + const SizedBox(height: 16), + Text( + isLoggedIn + ? 'No sessions recorded yet.' + : 'No sessions available.\n' + 'Please login to view the session history.', + textAlign: TextAlign.center, + style: TextStyle( + color: historyNoneColor, + fontSize: 16, + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/history_pod.dart b/lib/widgets/history_pod.dart new file mode 100644 index 0000000..1581759 --- /dev/null +++ b/lib/widgets/history_pod.dart @@ -0,0 +1,110 @@ +/// HistoryPod — the Pod read/write operations behind the InnerPod session +/// history, extracted from history.dart to keep that widget within the +/// project line-count limit. +/// +/// 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 + +library; + +import 'package:solidpod/solidpod.dart'; + +import 'package:innerpod/utils/local_session_store.dart'; +import 'package:innerpod/utils/session_logic.dart'; +import 'package:innerpod/widgets/history_backup.dart'; +import 'package:innerpod/widgets/history_format.dart'; + +/// The resource within the user's Pod that holds the session history. + +const sessionsFile = 'sessions.ttl'; + +/// True when [e] is the solidpod error raised before the security key is set. +/// +/// The caller then prompts for the key and retries the operation. + +bool isMissingKeyError(Object e) => + e.toString().contains('You must first set the security key!'); + +/// Read [sessionsFile], returning an empty session document when the resource +/// does not exist yet, which is normal for a new user. + +Future readSessions() async { + try { + return await readPod(sessionsFile); + } on ResourceNotExistException { + return serializeSessions([]); + } +} + +/// Overwrite [sessionsFile] with [content]. + +Future writeSessions(String content) => + writePod(sessionsFile, content, overwrite: true); + +/// Promote the locally-stored [session] to the Pod, then drop the local copy. + +Future podSyncSession(Map session) async { + // Reconstruct the raw session map for addSession. The duration is displayed + // as e.g. "20m", so convert it back to seconds for storage. + + final raw = { + 'start': session['rawStart'], + 'end': session['rawEnd'], + 'type': session['type'], + 'silenceDuration': durationToSeconds(session['duration']), + 'title': session['title'], + 'description': session['description'], + }; + + final content = await readSessions(); + await writeSessions(addSession(content, raw)); + await LocalSessionStore.removeSessionLocal(session['rawStart']!); +} + +/// Remove the session starting at [rawStart] from the Pod. + +Future podDeleteSession(String rawStart) async { + final content = await readSessions(); + await writeSessions(deleteSession(content, rawStart)); +} + +/// Remove every session from the Pod. + +Future podDeleteAllSessions() => writeSessions(serializeSessions([])); + +/// Apply [updates] to the Pod session starting at [rawStart]. + +Future podUpdateSession( + String rawStart, + Map updates, +) async { + final content = await readSessions(); + await writeSessions(updateSession(content, rawStart, updates)); +} + +/// Merge the sessions in [importedContent] into the Pod, skipping any whose +/// start time is already recorded. Returns the number of sessions added. + +Future podImportBackup(String importedContent) async { + final merged = mergeBackup(await readSessions(), importedContent); + await writeSessions(merged.content); + return merged.added; +} diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index e76b8d2..dbe5e96 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -553,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -634,7 +634,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -681,7 +681,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef64..b3c1761 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,9 +1,13 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index c49bc9c..3e1d37c 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -14,5 +14,7 @@ com.apple.security.keychain + com.apple.security.files.user-selected.read-write + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index c49bc9c..3e1d37c 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -14,5 +14,7 @@ com.apple.security.keychain + com.apple.security.files.user-selected.read-write + diff --git a/pubspec.yaml b/pubspec.yaml index ca25685..9539f08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,8 +29,8 @@ dependencies: markdown_tooltip: ^0.0.7 package_info_plus: ^10.2.1 shared_preferences: ^2.5.4 - solidpod: ^1.0.17 - solidui: ^1.0.34 + solidpod: ^1.0.20 + solidui: ^1.0.39 url_launcher: ^6.3.0 wakelock_plus: ^1.1.4