From c12faa3920335a53e2dc67f5cdf557cbb3f44bab Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 20 Jul 2026 16:32:24 +0200 Subject: [PATCH] app: package the desktop app as the web build, and delete the Linux embedder Completes the move: `linux/` is gone, replaced by the web build served by the wrapper in a Chrome `--app` window. The icons and the .desktop installer move out of `linux/` first -- they are not Flutter scaffolding and are still needed; the entry now points at the packaged launcher and matches the `--class=diet_guard_app` the wrapper passes, so the taskbar shows the app's icon rather than a browser's. install_arch.sh carries over the three traps each of which produced an install that looked fine until launched: `dart build cli` rather than `dart compile exe` (which refuses any package whose dependency graph has native build hooks), `options=('!strip')` (makepkg would otherwise discard the AOT snapshot, leaving a binary that runs as the bare Dart VM printing a usage message), and no browser in `depends` (this machine's policy uninstalls `chromium` seconds later, failing dependency resolution). Also adds the frame-cadence instrument the performance claim is measured with, armed by `--dart-define=DIET_GUARD_FRAME_STATS=1` and read with `String.fromEnvironment` -- `bool.fromEnvironment` only accepts the literal `true`, so `=1` would evaluate to false and a whole run would silently produce no data. It reports frame-to-frame cadence rather than build/raster durations, since work that fits the budget says nothing about the frame rate actually achieved. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 42 +++++ .../icons/128/diet-guard-app.png | Bin .../icons/16/diet-guard-app.png | Bin .../icons/24/diet-guard-app.png | Bin .../icons/256/diet-guard-app.png | Bin .../icons/32/diet-guard-app.png | Bin .../icons/48/diet-guard-app.png | Bin .../icons/512/diet-guard-app.png | Bin .../icons/64/diet-guard-app.png | Bin .../install_desktop_entry.sh | 40 ++--- app/install_arch.sh | 86 ++++++++++ app/lib/main.dart | 5 + app/lib/services/frame_stats.dart | 75 +++++++++ app/linux/.gitignore | 1 - app/linux/CMakeLists.txt | 128 --------------- app/linux/flutter/CMakeLists.txt | 88 ---------- .../flutter/generated_plugin_registrant.cc | 23 --- .../flutter/generated_plugin_registrant.h | 15 -- app/linux/flutter/generated_plugins.cmake | 27 ---- app/linux/runner/CMakeLists.txt | 26 --- app/linux/runner/main.cc | 6 - app/linux/runner/my_application.cc | 153 ------------------ app/linux/runner/my_application.h | 21 --- app/run.sh | 31 ++++ 24 files changed, 259 insertions(+), 508 deletions(-) rename app/{linux => desktop}/icons/128/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/16/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/24/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/256/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/32/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/48/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/512/diet-guard-app.png (100%) rename app/{linux => desktop}/icons/64/diet-guard-app.png (100%) rename app/{linux => desktop}/install_desktop_entry.sh (73%) create mode 100755 app/install_arch.sh create mode 100644 app/lib/services/frame_stats.dart delete mode 100644 app/linux/.gitignore delete mode 100644 app/linux/CMakeLists.txt delete mode 100644 app/linux/flutter/CMakeLists.txt delete mode 100644 app/linux/flutter/generated_plugin_registrant.cc delete mode 100644 app/linux/flutter/generated_plugin_registrant.h delete mode 100644 app/linux/flutter/generated_plugins.cmake delete mode 100644 app/linux/runner/CMakeLists.txt delete mode 100644 app/linux/runner/main.cc delete mode 100644 app/linux/runner/my_application.cc delete mode 100644 app/linux/runner/my_application.h create mode 100755 app/run.sh diff --git a/CLAUDE.md b/CLAUDE.md index 74c15c1..9163c33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,40 @@ silently does **not** reach the running service. private `kuhyx/syncs` GitHub repo (see "Cross-device sync" above) and `~/.config/diet_guard/sync_token`. +## The companion app's desktop target is a web build + +`app/` builds for two platforms, and only two: **Android** (the phone) and +**web** (the Linux desktop). There is no `app/linux/` — Flutter's GTK embedder +manages only ~20fps at 3840x2160 (measured on a stock `flutter create` app, so +it is the toolkit, not this code), where the same Dart in Chrome sustains +~144fps; see `~/todo/docs/desktop-performance-findings.md`. On the desktop the +app is the web build served by `bin/diet_guard_desktop.dart` and shown in a +Chrome `--app` window. + +Consequences worth knowing before touching `app/`: + +- **`dart:io` does not fail a web *compile*** — it becomes a stub that throws + at runtime, so the symptom is a blank white window, not a build error. Every + platform-specific edge goes behind a conditional export + (`document_store*.dart`, `blob_store*.dart`, `background_tasks*.dart`, + `notification_backend*.dart`, `token_vault*.dart`, + `github_client_factory*.dart`, `attached_image*.dart`). Branch on `kIsWeb` + *before* any `Platform.is…`, which itself throws on web. +- **The port and the Chrome `--user-data-dir` are fixed on purpose** + (`lib/services/desktop_wrapper.dart`). IndexedDB is keyed by origin and + lives in that profile: changing either hides the entire local food log. +- **The wrapper holds the GitHub token**, not the browser, and proxies + `api.github.com` plus the CORS-less device flow (`lib/desktop/github_proxy.dart`). +- **The desktop pushes as device id `desktop`**, the phone as `phone`, the + Python side as `pc`. Sharing an id means two devices overwrite each other's + pushed log every tick. +- **Desktop reminders only exist while the window is open.** A browser has no + WorkManager equivalent; the PC's real backstop is `diet-guard-gate.timer`, + which locks the screen. Don't "fix" this with a background service without + deciding you want a new daemon. +- `flutter create --platforms linux` would silently restore the slow path. + Don't run it in `app/`. + ## Commands - Run tests: `python -m pytest diet_guard/tests/ --cov=diet_guard --cov-branch --cov-fail-under=100` @@ -120,6 +154,11 @@ silently does **not** reach the running service. - Test the lock manually (safe, closeable): `python -m diet_guard gate --demo` - Run one sync tick manually: `python -m diet_guard sync` - Install for production: `bash install.sh` +- App tests: `cd app && flutter test` +- Run the desktop app from the repo: `cd app && bash run.sh` +- Install the desktop app (Arch package + .desktop entry): + `cd app && bash install_arch.sh` +- Build the phone app: `cd app && flutter build apk --release` ## Do NOT @@ -131,6 +170,9 @@ silently does **not** reach the running service. - Don't reintroduce a seal/lock/`chattr +i` on the budget file as a "fix" — it was deliberately removed so the budget is freely editable and synced between devices; see "Operational gotchas" above. +- Don't re-add a Linux embedder target to `app/`, and don't change the + wrapper's fixed port or Chrome profile path — see "The companion app's + desktop target is a web build" above. ## Flutter/Dart AI rules diff --git a/app/linux/icons/128/diet-guard-app.png b/app/desktop/icons/128/diet-guard-app.png similarity index 100% rename from app/linux/icons/128/diet-guard-app.png rename to app/desktop/icons/128/diet-guard-app.png diff --git a/app/linux/icons/16/diet-guard-app.png b/app/desktop/icons/16/diet-guard-app.png similarity index 100% rename from app/linux/icons/16/diet-guard-app.png rename to app/desktop/icons/16/diet-guard-app.png diff --git a/app/linux/icons/24/diet-guard-app.png b/app/desktop/icons/24/diet-guard-app.png similarity index 100% rename from app/linux/icons/24/diet-guard-app.png rename to app/desktop/icons/24/diet-guard-app.png diff --git a/app/linux/icons/256/diet-guard-app.png b/app/desktop/icons/256/diet-guard-app.png similarity index 100% rename from app/linux/icons/256/diet-guard-app.png rename to app/desktop/icons/256/diet-guard-app.png diff --git a/app/linux/icons/32/diet-guard-app.png b/app/desktop/icons/32/diet-guard-app.png similarity index 100% rename from app/linux/icons/32/diet-guard-app.png rename to app/desktop/icons/32/diet-guard-app.png diff --git a/app/linux/icons/48/diet-guard-app.png b/app/desktop/icons/48/diet-guard-app.png similarity index 100% rename from app/linux/icons/48/diet-guard-app.png rename to app/desktop/icons/48/diet-guard-app.png diff --git a/app/linux/icons/512/diet-guard-app.png b/app/desktop/icons/512/diet-guard-app.png similarity index 100% rename from app/linux/icons/512/diet-guard-app.png rename to app/desktop/icons/512/diet-guard-app.png diff --git a/app/linux/icons/64/diet-guard-app.png b/app/desktop/icons/64/diet-guard-app.png similarity index 100% rename from app/linux/icons/64/diet-guard-app.png rename to app/desktop/icons/64/diet-guard-app.png diff --git a/app/linux/install_desktop_entry.sh b/app/desktop/install_desktop_entry.sh similarity index 73% rename from app/linux/install_desktop_entry.sh rename to app/desktop/install_desktop_entry.sh index 3c568f3..dadff5a 100755 --- a/app/linux/install_desktop_entry.sh +++ b/app/desktop/install_desktop_entry.sh @@ -1,34 +1,34 @@ #!/bin/bash # ============================================================================ -# Install the diet_guard_app launcher icon and .desktop entry for the Linux desktop. +# Install the diet_guard_app launcher icon and .desktop entry for the Linux +# desktop. # -# Flutter's GTK runner has no icon of its own: my_application.cc calls -# gtk_window_set_icon_name("diet-guard-app"), which resolves through the hicolor icon -# theme. This script is what puts the icon into that theme, plus a .desktop -# entry so the app appears in application menus. +# The desktop app is a Flutter *web* build shown in a Chrome app window (the +# Linux embedder manages only ~20fps at 4K), so the launcher starts the +# wrapper, which serves the build and opens the browser. # -# Icon PNGs are pre-rendered and committed under linux/icons/, so this script +# Icon PNGs are pre-rendered and committed under desktop/icons/, so this script # needs no image tooling at run time. Regenerate them with: # PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons \ -# generate --app diet_guard_app --linux-out linux/icons +# generate --app diet_guard_app --linux-out desktop/icons # ============================================================================ set -euo pipefail SCRIPT_NAME="$(basename "$0")" -LINUX_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_DIR="$(dirname "$LINUX_DIR")" -readonly SCRIPT_NAME LINUX_DIR REPO_DIR +DESKTOP_SRC_DIR="$(cd "$(dirname "$0")" && pwd)" +readonly SCRIPT_NAME DESKTOP_SRC_DIR readonly ICON_NAME="diet-guard-app" -readonly BINARY_NAME="diet_guard_app" -# GTK derives WM_CLASS from the GApplication id in linux/CMakeLists.txt, -# not from the binary name; verified with xprop on the running window. -readonly WM_CLASS="com.kuhy.diet_guard_app" -readonly ICON_SRC_DIR="$LINUX_DIR/icons" +# Chrome sets WM_CLASS from --class, which the wrapper passes explicitly. +# Without it the window inherits the browser's own class and the taskbar shows +# a browser icon instead of this app's. +readonly WM_CLASS="diet_guard_app" +readonly ICON_SRC_DIR="$DESKTOP_SRC_DIR/icons" readonly ICON_THEME_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" readonly DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" -readonly BUNDLE_BIN="$REPO_DIR/build/linux/x64/release/bundle/$BINARY_NAME" +# Installed by install_arch.sh; the repo copy is only used for development. +readonly LAUNCHER="/usr/bin/diet-guard-app" usage() { echo "Usage: $SCRIPT_NAME" @@ -79,7 +79,7 @@ install_desktop_entry() { Type=Application Name=diet guard Comment=Diet logging and budget companion -Exec=$BUNDLE_BIN +Exec=$LAUNCHER Icon=$ICON_NAME Terminal=false Categories=Utility; @@ -97,9 +97,9 @@ main() { install_desktop_entry echo "Installed $ICON_NAME icon into $ICON_THEME_DIR" echo "Installed $DESKTOP_DIR/$ICON_NAME.desktop" - if [[ ! -x "$BUNDLE_BIN" ]]; then - echo "Note: $BUNDLE_BIN does not exist yet." - echo " Run 'flutter build linux --release' to make the entry launchable." + if [[ ! -x "$LAUNCHER" ]]; then + echo "Note: $LAUNCHER does not exist yet." + echo " Run 'bash install_arch.sh' to make the entry launchable." fi } diff --git a/app/install_arch.sh b/app/install_arch.sh new file mode 100755 index 0000000..5cf89e9 --- /dev/null +++ b/app/install_arch.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Builds the diet_guard companion app's desktop target and installs it as an +# Arch Linux package via pacman. +# Run from anywhere -- uses the directory of this script as the repo root. +# Requires: flutter, base-devel (provides makepkg), sudo for pacman. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# The desktop app is a Flutter *web* build served by a small local wrapper: +# Flutter's Linux embedder only reaches ~20fps at 4K, while the same Dart code +# in Chrome sustains ~144fps. +WEB_DIR="$SCRIPT_DIR/build/web" +WRAPPER_BUNDLE="$SCRIPT_DIR/build/cli/bundle" +WORK_DIR="$(mktemp -d)" + +cleanup() { rm -rf "$WORK_DIR"; } +trap cleanup EXIT + +# Parse version from pubspec.yaml (strip build number: 1.0.0+1 -> 1.0.0) +PKGVER="$(grep '^version:' "$SCRIPT_DIR/pubspec.yaml" \ + | sed 's/^version:[[:space:]]*//' | sed 's/+.*//')" + +echo "==> Building diet_guard_app $PKGVER (Flutter web release)..." +cd "$SCRIPT_DIR" +flutter build web --release + +echo "==> Compiling the desktop wrapper..." +# `dart build cli`, not `dart compile exe`: the package pulls in dependencies +# with native build hooks, which `dart compile` refuses to handle even though +# the wrapper itself uses none of them. +rm -rf "$SCRIPT_DIR/build/cli" +dart build cli -o "$SCRIPT_DIR/build/cli" + +echo "==> Generating PKGBUILD..." +cat > "$WORK_DIR/PKGBUILD" < "\$pkgdir/usr/bin/diet-guard-app" <<'WRAPPER' +#!/bin/bash +exec /opt/diet-guard-app/bin/diet_guard_desktop "\$@" +WRAPPER + chmod 755 "\$pkgdir/usr/bin/diet-guard-app" +} +EOF + +echo "==> Installing package via makepkg..." +cd "$WORK_DIR" +makepkg -sif --noconfirm + +echo "==> Installing the desktop entry..." +"$SCRIPT_DIR/desktop/install_desktop_entry.sh" + +echo "==> Done. 'diet-guard-app' now runs version $PKGVER installed via pacman." diff --git a/app/lib/main.dart b/app/lib/main.dart index 476f0df..f1965c7 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -8,6 +8,7 @@ import 'package:diet_guard_app/screens/log_meal_screen.dart'; import 'package:diet_guard_app/services/app_settings_service.dart'; import 'package:diet_guard_app/services/background_tasks.dart'; import 'package:diet_guard_app/services/foodbank_service.dart'; +import 'package:diet_guard_app/services/frame_stats.dart'; import 'package:diet_guard_app/services/log_storage_service.dart'; import 'package:diet_guard_app/services/notification_service.dart'; import 'package:flutter/material.dart'; @@ -32,6 +33,10 @@ Future main() async { }), ); // coverage:ignore-end + // Off unless armed with --dart-define=DIET_GUARD_FRAME_STATS=1. + // coverage:ignore-start + if (frameStatsEnabled) startFrameStats(); + // coverage:ignore-end runApp(const DietGuardApp()); } diff --git a/app/lib/services/frame_stats.dart b/app/lib/services/frame_stats.dart new file mode 100644 index 0000000..ab3227f --- /dev/null +++ b/app/lib/services/frame_stats.dart @@ -0,0 +1,75 @@ +/// Optional frame-cadence instrument, armed with +/// `--dart-define=DIET_GUARD_FRAME_STATS=1`. +/// +/// Reports **frame-to-frame cadence**, not build/raster durations: work that +/// fits the budget says nothing about the frame rate actually achieved, which +/// is the number the desktop migration exists to move. +/// +/// Read with `String.fromEnvironment`, never `bool.fromEnvironment` -- the +/// latter only accepts the literal `true`, so `=1` would silently evaluate to +/// false and a whole measurement run would produce no data while looking like +/// a broken callback. +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui' show FramePhase; + +import 'package:diet_guard_app/services/desktop_wrapper.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:http/http.dart' as http; + +/// Whether the instrument is armed for this build. +const frameStatsEnabled = + String.fromEnvironment('DIET_GUARD_FRAME_STATS') == '1'; + +/// Starts collecting frame-to-frame intervals and periodically posts a +/// summary to the desktop wrapper, which writes it to disk. +/// +/// The wrapper is used as the sink because a browser cannot write a file and +/// reading its console from outside needs a debugger attached; a POST lands +/// somewhere a measurement run can simply read. +// coverage:ignore-start +void startFrameStats({Duration reportEvery = const Duration(seconds: 2)}) { + final intervalsMs = []; + int? previousMicros; + + SchedulerBinding.instance.addTimingsCallback((timings) { + for (final timing in timings) { + final micros = timing.timestampInMicroseconds(FramePhase.rasterFinish); + if (previousMicros != null) { + intervalsMs.add((micros - previousMicros!) / 1000); + } + previousMicros = micros; + } + }); + + Timer.periodic(reportEvery, (_) { + if (intervalsMs.isEmpty) return; + final sample = [...intervalsMs]..sort(); + intervalsMs.clear(); + double percentile(double fraction) => + sample[(sample.length * fraction).floor().clamp( + 0, + sample.length - 1, + )]; + unawaited( + http.post( + Uri.parse( + '$desktopWrapperOrigin${WrapperPaths.documents}' + 'frame_stats.json', + ), + body: jsonEncode({ + 'frames': sample.length, + 'p50_ms': percentile(0.50), + 'p95_ms': percentile(0.95), + 'max_ms': sample.last, + 'fps_at_p50': 1000 / percentile(0.50), + 'fps_at_p95': 1000 / percentile(0.95), + }), + ), + ); + }); +} + +// coverage:ignore-end diff --git a/app/linux/.gitignore b/app/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/app/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/app/linux/CMakeLists.txt b/app/linux/CMakeLists.txt deleted file mode 100644 index d0e8eea..0000000 --- a/app/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "diet_guard_app") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.kuhy.diet_guard_app") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/app/linux/flutter/CMakeLists.txt b/app/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/app/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 3ccd551..0000000 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,23 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); - file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); - flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/app/linux/flutter/generated_plugin_registrant.h b/app/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/app/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake deleted file mode 100644 index fbedf4a..0000000 --- a/app/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - file_selector_linux - flutter_secure_storage_linux - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/app/linux/runner/CMakeLists.txt b/app/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc..0000000 --- a/app/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/app/linux/runner/main.cc b/app/linux/runner/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/app/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/app/linux/runner/my_application.cc b/app/linux/runner/my_application.cc deleted file mode 100644 index 6506bf6..0000000 --- a/app/linux/runner/my_application.cc +++ /dev/null @@ -1,153 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "diet_guard_app"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "diet_guard_app"); - } - - gtk_window_set_default_size(window, 1280, 720); - - // Resolved from the hicolor icon theme, which linux/install_desktop_entry.sh - // populates from linux/icons/. Without this the GTK runner shows a generic - // placeholder in the taskbar, since Flutter never sets a window icon itself. - gtk_window_set_icon_name(window, "diet-guard-app"); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/app/linux/runner/my_application.h b/app/linux/runner/my_application.h deleted file mode 100644 index db16367..0000000 --- a/app/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/app/run.sh b/app/run.sh new file mode 100755 index 0000000..0b50930 --- /dev/null +++ b/app/run.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Builds the web bundle and launches the diet_guard companion app through the +# desktop wrapper, straight out of the repo (no packaging step). +# +# The desktop app is a Flutter *web* build rather than a Linux embedder build: +# the embedder manages only ~20fps at 4K, while the same Dart code in Chrome +# sustains ~144fps. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR + +log() { echo "==> $*"; } + +command -v flutter >/dev/null 2>&1 || { + echo "flutter is not on PATH" >&2 + exit 1 +} + +cd "$SCRIPT_DIR" +flutter config --enable-web >/dev/null + +log "fetching pub packages" +flutter pub get + +log "building the web bundle" +flutter build web --release + +log "launching the app" +exec dart run bin/diet_guard_desktop.dart --web-root "$SCRIPT_DIR/build/web"