summaryrefslogtreecommitdiff
path: root/lib/services
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-08 00:05:39 +0300
committerPaul Buetow <paul@buetow.org>2026-05-08 00:05:39 +0300
commit3e4bacb7b939b1de98a2fb07115f638750637357 (patch)
tree0897a6854d1c3ca3b1d0252657e992abbba061ba /lib/services
parent665b150727ee7259e398d38d1f6bc51b4c2d4a1c (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.dart57
-rw-r--r--lib/services/preferences.dart30
-rw-r--r--lib/services/share_service.dart25
-rw-r--r--lib/services/shared_text_handler.dart57
-rw-r--r--lib/services/storage.dart14
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;
+}