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 | |
| 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>
66 files changed, 2561 insertions, 1612 deletions
@@ -1,3 +1,34 @@ -fyne-cross -quicklogger -tmp-pkg +# Dart / Flutter +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +.dart_tool/ +pubspec.lock.bak + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ + +# Android +android/.gradle/ +android/local.properties +android/captures/ +android/.cxx/ +android/key.properties +*.jks +*.keystore + +# Linux +linux/flutter/ephemeral/ + +# Misc +.DS_Store +.claude/ +.serena/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..d137bb0 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: android + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: linux + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/FyneApp.toml b/FyneApp.toml deleted file mode 100644 index 2d15de9..0000000 --- a/FyneApp.toml +++ /dev/null @@ -1,8 +0,0 @@ -Website = "https://codeberg.org/snonux/quicklogger" - -[Details] - Icon = "icon.png" - Name = "quicklogger" - ID = "org.buetow.quicklogger" - Version = "0.1.1" - Build = 64 diff --git a/Magefile.go b/Magefile.go deleted file mode 100644 index ef25814..0000000 --- a/Magefile.go +++ /dev/null @@ -1,201 +0,0 @@ -//go:build mage -// +build mage - -package main - -import ( - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strconv" - "time" - - "github.com/magefile/mage/sh" -) - -// Build compiles the quicklogger app into ./bin. -func Build() error { - outDir := "bin" - if err := os.MkdirAll(outDir, 0o755); err != nil { - return err - } - - binName := "quicklogger" - if runtime.GOOS == "windows" { - binName += ".exe" - } - - out := filepath.Join(outDir, binName) - fmt.Printf("Building %s\n", out) - return sh.RunV("go", "build", "-o", out, ".") -} - -// Run launches the app with `go run .`. -func Run() error { - fmt.Println("Running quicklogger (verbose build)") - // Show Go version and key env for diagnostics - _ = sh.RunV("go", "version") - _ = sh.RunV("go", "env", "GOVERSION", "GOOS", "GOARCH", "GOMOD", "GOMODCACHE", "GOCACHE") - // Compile+run with verbose and trace flags to show build steps - return sh.RunV("go", "run", "-v", "-x", ".") -} - -// Clean removes build artifacts (bin/ and local APK). -func Clean() error { - fmt.Println("Cleaning build artifacts") - // Remove bin directory - if err := os.RemoveAll("bin"); err != nil { - return err - } - // Remove generated APK if present - _ = os.Remove("quicklogger.apk") - return nil -} - -// Android packages an Android APK via Fyne and copies it to ~/Documents/APKs if present. -func Android() error { - env := map[string]string{} - - // Respect existing ANDROID_NDK_HOME. If not set, try legacy var or a common default path. - ndk := os.Getenv("ANDROID_NDK_HOME") - if ndk == "" { - // Some setups export a misspelled var; honor it if present. - ndk = os.Getenv("ANDRIOD_NDK_HOME") - } - if ndk == "" { - if home, err := os.UserHomeDir(); err == nil { - guess := filepath.Join(home, "android-ndk", "android-ndk-r26b") - env["ANDROID_NDK_HOME"] = guess - fmt.Printf("ANDROID_NDK_HOME not set, guessing %s\n", guess) - } - } - - fmt.Println("Packaging Android APK via Fyne") - if err := sh.RunWithV(env, "fyne", "package", "-os", "android"); err != nil { - return err - } - - // If ~/Documents/APKs exists, copy the APK there. - home, err := os.UserHomeDir() - if err != nil { - return nil // packaging succeeded; copying is best-effort - } - destDir := filepath.Join(home, "Documents", "APKs") - if st, err := os.Stat(destDir); err == nil && st.IsDir() { - src := "quicklogger.apk" - if _, err := os.Stat(src); err == nil { - dst := filepath.Join(destDir, filepath.Base(src)) - if err := copyFile(src, dst); err != nil { - return err - } - fmt.Printf("Copied %s to %s\n", src, dst) - } - } - return nil -} - -// AndroidCross builds an Android APK using fyne-cross with Docker/Podman. -// Mirrors steps from README using a Podman socket if available. -func AndroidCross() error { - env := map[string]string{} - - // If DOCKER_HOST is not set, try a sensible Podman socket default. - if os.Getenv("DOCKER_HOST") == "" { - uid := os.Geteuid() - sock := filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock") - if _, err := os.Stat(sock); err == nil { - env["DOCKER_HOST"] = "unix://" + sock - fmt.Printf("Using Podman socket: %s\n", env["DOCKER_HOST"]) - } else { - fmt.Println("DOCKER_HOST is not set and no Podman socket found; relying on default Docker daemon.") - } - } - - // Ensure fyne-cross is available; attempt install if not. - if _, err := sh.Output("which", "fyne-cross"); err != nil { - fmt.Println("Installing fyne-cross (requires network access)...") - if err := sh.RunV("go", "install", "github.com/fyne-io/fyne-cross@latest"); err != nil { - return fmt.Errorf("fyne-cross not found and installation failed: %w", err) - } - } - - // Pull/update builder image then build. - if err := sh.RunWithV(env, "fyne-cross", "android", "--pull"); err != nil { - return err - } - if err := sh.RunWithV(env, "fyne-cross", "android"); err != nil { - return err - } - - // Copy newest APK to ~/Documents/APKs if available. - apk, err := newestAPK("fyne-cross/dist/android") - if err != nil { - // Not fatal; print a note and finish - fmt.Printf("Note: could not locate built APK: %v\n", err) - return nil - } - home, herr := os.UserHomeDir() - if herr != nil { - return nil - } - destDir := filepath.Join(home, "Documents", "APKs") - if st, err := os.Stat(destDir); err == nil && st.IsDir() { - dst := filepath.Join(destDir, filepath.Base(apk)) - if err := copyFile(apk, dst); err != nil { - return err - } - fmt.Printf("Copied %s to %s\n", apk, dst) - } - return nil -} - -func newestAPK(dir string) (string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - return "", err - } - var newest string - var newestMod time.Time - for _, e := range entries { - if e.IsDir() { - continue - } - name := e.Name() - if filepath.Ext(name) != ".apk" { - continue - } - info, err := e.Info() - if err != nil { - continue - } - if info.ModTime().After(newestMod) { - newestMod = info.ModTime() - newest = filepath.Join(dir, name) - } - } - if newest == "" { - return "", fmt.Errorf("no .apk found in %s", dir) - } - return newest, nil -} - -func copyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer func() { _ = out.Close() }() - - if _, err = io.Copy(out, in); err != nil { - return err - } - return out.Sync() -} @@ -1,83 +1,96 @@ -# Quick logger +# Quicklog - + -This is a tiny GUI app written in Go using the Fyne framework to quickly log a message to a file. Read on my blog more about this: https://foo.zone/gemfeed/2024-03-03-a-fine-fyne-android-app-for-quickly-logging-ideas-programmed-in-golang.html -
-The purpose of this is to have a small Android app to quickly log Ideas into a folder as plain text files. From there, Syncthing will sync it to my computer at home. +Tiny GUI app to quickly jot a thought into a timestamped Markdown file. +Originally a Go/Fyne app called *Quicklogger* — this is the Flutter rewrite, +renamed to **Quicklog**, targeting Android (primary) and Linux desktop +(development). -This are screenshots of the App running on Android and Fedora Linux. +The intent is the same as before: type a quick note on Android, hit **Log +text**, and let Syncthing copy the resulting `ql-YYMMDD-HHMMSS.md` file to your +home computer.   -## Build and Run (Mage) +## Features -This repo includes Mage tasks to build, run and cross‑compile. +- Single-screen text editor with character counter and a one-shot warning at + 5,000 characters. +- Each press of **Log text** writes the current text to a new file named + `ql-YYMMDD-HHMMSS.md` in the configured directory. +- **Preferences**: configurable log directory and an "Auto-log shared text" + toggle. +- **Entry browser**: list of previous entries (newest first) with read-only + viewer and long-press to delete. +- **Share to Quicklog** on Android: share text from any app and Quicklog + either prefills the editor or logs it immediately, depending on the + preference. -Install Mage: +## Requirements -```sh -go install github.com/magefile/mage@latest -``` +- [Flutter](https://flutter.dev) stable channel (3.41+). +- For Android builds: Android SDK + JDK 17 (Flutter's Gradle does not yet + support newer JDKs). On Fedora, GraalVM 17 works — point Flutter at it with + `flutter config --jdk-dir=/usr/lib/jvm/graalvm-community-openjdk-17.0.9+9.1`. +- For Linux desktop builds: `gtk3-devel`, `mesa-demos`, `clang`, `cmake`, + `ninja-build`, `pkg-config`, `xz-devel`. -Clone and enter the repo: +## Build and Run + +### Linux desktop ```sh -git clone https://codeberg.org/snonux/quicklogger -cd quicklogger +flutter run -d linux # dev with hot reload +flutter build linux --release # release bundle: build/linux/x64/release/bundle/ ``` -Common tasks: +### Android ```sh -# Build desktop binary into ./bin -mage build - -# Run the app (shows verbose Go build output) -mage run - -# Clean build artifacts -mage clean +flutter run -d <device-id> # dev on a connected device +flutter build apk --release # fat APK with all ABIs ``` -## Android Builds - -Two options exist: local Fyne packaging or containerized cross‑compile. - -- Local packaging (requires Fyne CLI and Android NDK): +### Cross-compile from amd64 Linux to ARM Android APK - ```sh - # Install Fyne CLI if needed - go install fyne.io/fyne/v2/cmd/fyne@latest +Unlike the previous Fyne build, no Docker / Podman / `fyne-cross` / NDK is +needed. Flutter's Dart AOT compiler emits ARM machine code directly from x86_64. - # Ensure ANDROID_NDK_HOME points to your NDK (e.g. ~/android-ndk/android-ndk-r26b) - export ANDROID_NDK_HOME=~/android-ndk/android-ndk-r26b - - # Build APK in the project root as quicklogger.apk - mage android - ``` - -- Containerized cross‑compile (recommended, uses fyne-cross with Docker/Podman): +```sh +# Per-ABI split APKs (smaller, recommended for distribution) +flutter build apk --release --split-per-abi +# → build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (modern phones) +# → build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (older 32-bit) +# → build/app/outputs/flutter-apk/app-x86_64-release.apk (emulators) + +# ARM64 only +flutter build apk --release --target-platform android-arm64 +``` - ```sh - # Start Podman if you prefer Podman over Docker - sudo systemctl start podman +Install on the device: - # The task auto-detects a user Podman socket; otherwise it uses Docker defaults - mage androidcross - ``` +```sh +adb install -r build/app/outputs/flutter-apk/app-arm64-v8a-release.apk +``` -After cross‑compiling, the APK is located at `fyne-cross/dist/android/quicklogger.apk`. -Copy it to your device and install it (you may need to allow installing from unknown sources): +### Tests ```sh -adb install -r fyne-cross/dist/android/quicklogger.apk -# or copy manually and install on device +flutter test +flutter analyze ``` -## Share with QuickLogger on Android +## Share with Quicklog on Android + +From any app that can share text, choose **Share** → **Quicklog**. With +auto-log off (default), the text opens in the editor for review. Toggle +"Auto-log shared text" in Preferences to have shared text written +straight to disk. -From any Android app that can share text, choose **Share** and send it to QuickLogger. The text opens in the editor so you can review it, edit it, or tap **Log text**. +## Storage on Android -If you want shared text to be saved immediately, open **Preferences** and enable **Auto-log shared text**. With that on, shared text goes straight to your log directory instead of being prefilled. +Log files are written to the app-specific external directory at +`/Android/data/org.buetow.quicklog/files/`. No storage permissions are +required; point Syncthing at that folder to sync to your home computer. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml deleted file mode 100644 index 0e6115e..0000000 --- a/android/AndroidManifest.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="org.buetow.quicklogger"> - - <application> - <!-- Also accept SEND on the main Go activity (some launchers prefer direct targets) --> - <activity - android:name="org.golang.app.GoNativeActivity" - android:exported="true" - tools:node="merge"> - <intent-filter> - <action android:name="android.intent.action.SEND" /> - <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="text/plain" /> - <data android:mimeType="text/*" /> - </intent-filter> - </activity> - - <!-- Share receiver activity: writes text to cache and launches main app --> - <activity - android:name=".ShareActivity" - android:exported="true" - android:excludeFromRecents="true" - android:launchMode="singleTask" - android:taskAffinity="" - android:theme="@android:style/Theme.Translucent.NoTitleBar"> - <intent-filter> - <action android:name="android.intent.action.SEND" /> - <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="text/plain" /> - <data android:mimeType="text/*" /> - </intent-filter> - </activity> - </application> -</manifest> diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..3333b8a --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "org.buetow.quicklog" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "org.buetow.quicklog" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- The INTERNET permission is required for development. Specifically, + the Flutter tool needs it to communicate with the running application + to allow setting breakpoints, to provide hot reload, etc. + --> + <uses-permission android:name="android.permission.INTERNET"/> +</manifest> diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6eddff0 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <application + android:label="Quicklog" + android:name="${applicationName}" + android:icon="@mipmap/ic_launcher"> + <activity + android:name=".MainActivity" + android:exported="true" + android:launchMode="singleTop" + android:taskAffinity="" + android:theme="@style/LaunchTheme" + android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" + android:hardwareAccelerated="true" + android:windowSoftInputMode="adjustResize"> + <meta-data + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + & |
