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/services | |
| 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/services')
| -rw-r--r-- | lib/services/log_service.dart | 57 | ||||
| -rw-r--r-- | lib/services/preferences.dart | 30 | ||||
| -rw-r--r-- | lib/services/share_service.dart | 25 | ||||
| -rw-r--r-- | lib/services/shared_text_handler.dart | 57 | ||||
| -rw-r--r-- | lib/services/storage.dart | 14 |
5 files changed, 183 insertions, 0 deletions
diff --git a/lib/services/log_service.dart b/lib/services/log_service.dart new file mode 100644 index 0000000..f2f662c --- /dev/null +++ b/lib/services/log_service.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:intl/intl.dart'; +import 'package:path/path.dart' as p; + +class LogEntry { + LogEntry({required this.file, required this.timestamp}); + final File file; + final DateTime timestamp; +} + +final _filenameRegex = RegExp(r'^ql-(\d{6})-(\d{6})\.md$'); +final _timestampFormat = DateFormat('yyMMdd-HHmmss'); + +Future<File> logEntry(String dir, String text, {DateTime? now}) async { + final stamp = _timestampFormat.format(now ?? DateTime.now()); + final file = File(p.join(dir, 'ql-$stamp.md')); + await file.writeAsString(text); + return file; +} + +Future<List<LogEntry>> listEntries(String dir) async { + final directory = Directory(dir); + if (!await directory.exists()) return const []; + final entries = <LogEntry>[]; + await for (final entity in directory.list(followLinks: false)) { + if (entity is! File) continue; + final ts = _parseFilename(p.basename(entity.path)); + if (ts == null) continue; + entries.add(LogEntry(file: entity, timestamp: ts)); + } + entries.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + return entries; +} + +Future<void> deleteEntry(File f) async { + if (await f.exists()) await f.delete(); +} + +DateTime? _parseFilename(String name) { + final m = _filenameRegex.firstMatch(name); + if (m == null) return null; + final d = m.group(1)!; // YYMMDD + final t = m.group(2)!; // HHMMSS + try { + return DateTime( + 2000 + int.parse(d.substring(0, 2)), + int.parse(d.substring(2, 4)), + int.parse(d.substring(4, 6)), + int.parse(t.substring(0, 2)), + int.parse(t.substring(2, 4)), + int.parse(t.substring(4, 6)), + ); + } catch (_) { + return null; + } +} diff --git a/lib/services/preferences.dart b/lib/services/preferences.dart new file mode 100644 index 0000000..dbe261a --- /dev/null +++ b/lib/services/preferences.dart @@ -0,0 +1,30 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +import 'storage.dart'; + +const _kDirectory = 'Directory'; +const _kAutoLogSharedText = 'AutoLogSharedText'; + +class PreferencesService { + Future<String> directory() async { + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getString(_kDirectory); + if (stored != null && stored.isNotEmpty) return stored; + return defaultLogDirectory(); + } + + Future<bool> autoLogSharedText() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_kAutoLogSharedText) ?? false; + } + + Future<void> setDirectory(String value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_kDirectory, value); + } + + Future<void> setAutoLogSharedText(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_kAutoLogSharedText, value); + } +} diff --git a/lib/services/share_service.dart b/lib/services/share_service.dart new file mode 100644 index 0000000..36eb7af --- /dev/null +++ b/lib/services/share_service.dart @@ -0,0 +1,25 @@ +import 'dart:io' show Platform; + +import 'package:flutter/services.dart'; + +class ShareService { + static const _channel = MethodChannel('org.buetow.quicklog/share'); + + static Future<String?> readSharedTextFromCache() async { + if (!Platform.isAndroid) return null; + try { + return await _channel.invokeMethod<String>('readSharedTextFromCache'); + } on MissingPluginException { + return null; + } + } + + static Future<void> clearSharedTextCache() async { + if (!Platform.isAndroid) return; + try { + await _channel.invokeMethod<void>('clearSharedTextCache'); + } on MissingPluginException { + // No-op: channel not registered (e.g. running on a non-Android target). + } + } +} diff --git a/lib/services/shared_text_handler.dart b/lib/services/shared_text_handler.dart new file mode 100644 index 0000000..c8e99a4 --- /dev/null +++ b/lib/services/shared_text_handler.dart @@ -0,0 +1,57 @@ +enum SharedTextLoadMode { prefill, autoLog } + +class SharedTextDecision { + const SharedTextDecision({required this.mode, required this.text, required this.proceed}); + final SharedTextLoadMode mode; + final String text; + final bool proceed; +} + +SharedTextDecision prepareSharedTextLoad(String text, bool autoLog) { + if (text.trim().isEmpty) { + return const SharedTextDecision(mode: SharedTextLoadMode.prefill, text: '', proceed: false); + } + return SharedTextDecision( + mode: autoLog ? SharedTextLoadMode.autoLog : SharedTextLoadMode.prefill, + text: text, + proceed: true, + ); +} + +typedef LogFn = Future<void> Function(String dir, String text); +typedef ShowInfo = void Function(String title, String message); +typedef ShowError = void Function(Object error); + +Future<void> handleSharedTextLoad({ + required String text, + required bool autoLog, + required String dir, + required void Function(String) prefill, + required void Function() focus, + required void Function() resetInput, + required Future<void> Function() clearCache, + required LogFn logFn, + required ShowInfo showInfo, + required ShowError showError, +}) async { + final decision = prepareSharedTextLoad(text, autoLog); + if (!decision.proceed) { + await clearCache(); + return; + } + if (decision.mode == SharedTextLoadMode.autoLog) { + try { + await logFn(dir, decision.text); + } catch (e) { + showError(e); + return; + } + showInfo('Logged', 'Shared text has been logged.'); + resetInput(); + await clearCache(); + return; + } + prefill(decision.text); + focus(); + await clearCache(); +} diff --git a/lib/services/storage.dart b/lib/services/storage.dart new file mode 100644 index 0000000..e5a0f50 --- /dev/null +++ b/lib/services/storage.dart @@ -0,0 +1,14 @@ +import 'dart:io' show Directory, Platform; + +import 'package:path_provider/path_provider.dart'; + +const String defaultLinuxDirectory = '.'; + +Future<String> defaultLogDirectory() async { + if (Platform.isAndroid) { + final dir = await getExternalStorageDirectory(); + if (dir != null) return dir.path; + return (await getApplicationDocumentsDirectory()).path; + } + return Directory.current.path; +} |
