diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-08 00:05:39 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-08 00:05:39 +0300 |
| commit | 3e4bacb7b939b1de98a2fb07115f638750637357 (patch) | |
| tree | 0897a6854d1c3ca3b1d0252657e992abbba061ba /lib/screens/entry_browser_screen.dart | |
| parent | 665b150727ee7259e398d38d1f6bc51b4c2d4a1c (diff) | |
Rewrite as Flutter app and rename to Quicklogmain
Drop the Go/Fyne implementation and replace it with a native Flutter app
targeting Android (primary) and Linux desktop (development). Rename
quicklogger -> quicklog throughout (project, applicationId, MethodChannel,
cache filename, AppBar title, Linux binary).
Storage on Android moves to /Android/data/org.buetow.quicklog/files/ so
MANAGE_EXTERNAL_STORAGE is no longer required. Share-intent receive is
preserved via a Kotlin ShareActivity that writes shared text to the app
cache, which Dart reads through a MethodChannel on app resume; MainActivity
also accepts SEND directly so launchers that prefer the main target work.
New since the Fyne version: an entry browser screen with read-only viewer
and long-press delete.
Cross-compiling to Android ARM from amd64 Linux no longer needs Docker /
fyne-cross / NDK -- flutter build apk emits ARM binaries directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'lib/screens/entry_browser_screen.dart')
| -rw-r--r-- | lib/screens/entry_browser_screen.dart | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/lib/screens/entry_browser_screen.dart b/lib/screens/entry_browser_screen.dart new file mode 100644 index 0000000..5ffa707 --- /dev/null +++ b/lib/screens/entry_browser_screen.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart' as p; + +import '../services/log_service.dart'; +import '../services/preferences.dart'; + +final _displayFormat = DateFormat('yyyy-MM-dd HH:mm:ss'); + +class EntryBrowserScreen extends StatefulWidget { + const EntryBrowserScreen({super.key}); + + @override + State<EntryBrowserScreen> createState() => _EntryBrowserScreenState(); +} + +class _EntryBrowserScreenState extends State<EntryBrowserScreen> { + final PreferencesService _prefs = PreferencesService(); + Future<List<LogEntry>>? _future; + String _dir = ''; + + @override + void initState() { + super.initState(); + _refresh(); + } + + void _refresh() { + setState(() { + _future = _load(); + }); + } + + Future<List<LogEntry>> _load() async { + _dir = await _prefs.directory(); + return listEntries(_dir); + } + + Future<void> _open(LogEntry entry) async { + final content = await entry.file.readAsString(); + if (!mounted) return; + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => _EntryDetailScreen(entry: entry, content: content), + ), + ); + } + + Future<void> _confirmDelete(LogEntry entry) async { + final ok = await showDialog<bool>( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Delete entry?'), + content: Text(p.basename(entry.file.path)), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Delete')), + ], + ), + ); + if (ok == true) { + await deleteEntry(entry.file); + _refresh(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Entries'), + actions: [ + IconButton( + tooltip: 'Refresh', + icon: const Icon(Icons.refresh), + onPressed: _refresh, + ), + ], + ), + body: FutureBuilder<List<LogEntry>>( + future: _future, + builder: (ctx, snap) { + if (snap.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + if (snap.hasError) { + return Center(child: Text('Error: ${snap.error}')); + } + final entries = snap.data ?? const []; + if (entries.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'No entries in $_dir', + textAlign: TextAlign.center, + ), + ), + ); + } + return RefreshIndicator( + onRefresh: () async => _refresh(), + child: ListView.separated( + itemCount: entries.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => _EntryTile( + entry: entries[i], + onTap: () => _open(entries[i]), + onLongPress: () => _confirmDelete(entries[i]), + ), + ), + ); + }, + ), + ); + } +} + +class _EntryTile extends StatelessWidget { + const _EntryTile({required this.entry, required this.onTap, required this.onLongPress}); + final LogEntry entry; + final VoidCallback onTap; + final VoidCallback onLongPress; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(_displayFormat.format(entry.timestamp)), + subtitle: FutureBuilder<String>( + future: _firstLine(entry.file), + builder: (_, snap) => Text( + snap.data ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + onTap: onTap, + onLongPress: onLongPress, + ); + } + + Future<String> _firstLine(File f) async { + try { + final content = await f.readAsString(); + final i = content.indexOf('\n'); + return i < 0 ? content : content.substring(0, i); + } catch (_) { + return ''; + } + } +} + +class _EntryDetailScreen extends StatelessWidget { + const _EntryDetailScreen({required this.entry, required this.content}); + final LogEntry entry; + final String content; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(p.basename(entry.file.path))), + body: SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: SelectableText(content), + ), + ); + } +} |
