Run the workout app on Linux, and let it hold the screen lock

The phone is lost, so no workout can be recorded and enforcement has been
disarmed since 2026-08-21. This gives the PC a first-class way to do the
workout, reusing the existing app rather than reimplementing the progression
rules a second time -- two implementations would guarantee the divergence the
"same weights everywhere" requirement forbids.

Storage: sqflite has no Linux plugin, so main() installs sqflite_common_ffi's
factory, which runs the same schema and migrations against the same SQL. It
was already a dev dependency driving the test suite; this only promotes it.

Auth: the account password in ~/.config/crdt-sync no longer authenticates
(verified: HTTP 400 INVALID_LOGIN_CREDENTIALS). The PC fleet has been running
on the cached refresh token all along, so the desktop credential store reads
that same 0600 file -- the Dart and Python credential JSON keys are already
identical -- and passes null instead of the stale password. This also avoids
google_sign_in, which ships no Linux implementation.

Lock mode: --lock-mode makes the window the lock surface. There is no Dart API
for this, so the runner sets override-redirect and takes an exclusive
gdk_seat_grab. Two details cost a debugging cycle each and are commented in
place: override-redirect must be set BEFORE the first map, or the window stays
mapped and never repaints; and gtk_window_fullscreen() only ASKS the window
manager, which cannot see an override-redirect window -- so the geometry is
set from the monitor directly, otherwise a strip of live desktop is left
uncovered at the edge of a larger screen.

The grab is acquired by retry rather than by asking the supervisor to release
first: the Python locker keeps its grab while this window maps on top, so the
screen is covered continuously instead of exposing the desktop for the whole
of Flutter's cold start.

Verified against the live data: pulls all 7 exercises from Firebase (Bench
17.5x12, RDL 27.5x8, Row 25x6, Situp 10x31 -- the real weights, not factory
defaults), leaves the remote progression byte-identical, and mints its own
device id rather than reusing the phone's. A Tk client is refused the grab
while the lock holds it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg
This commit is contained in:
Krzysztof Rudnicki 2026-08-22 11:18:48 +02:00
parent 2c206e5070
commit 186bfcabc4
21 changed files with 952 additions and 170 deletions

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
revision: "4cf24164269a5ebf0c16a028a00727d0e77bbb05"
channel: "stable"
project_type: app
@ -13,11 +13,14 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
create_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
base_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
- platform: android
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
create_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
base_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
- platform: linux
create_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
base_revision: 4cf24164269a5ebf0c16a028a00727d0e77bbb05
# User provided section

View File

@ -1,9 +1,12 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:workout_app/screens/home_screen.dart';
import 'package:workout_app/services/backup_service.dart';
import 'package:workout_app/services/http_server_service.dart';
import 'package:workout_app/services/lock_mode.dart';
import 'package:workout_app/services/progression_sync_service.dart';
import 'package:workout_app/services/storage_service.dart';
import 'package:workout_app/services/sync_device_id.dart';
@ -13,8 +16,18 @@ import 'package:workout_app/ui/theme.dart';
// App bootstrap: permission request, DB init, and a real listening socket —
// platform-channel work that can't run on the CI test host. The pieces it wires
// up are unit-tested individually; the entry point itself is not.
void main() async {
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
// Must precede runApp: the UI reads this to decide whether to offer
// any way out of the workout.
lockModeEnabled = parseLockMode(args);
// Linux desktop has no sqflite plugin. The FFI factory runs the same
// schema and migrations against the same SQL, so this is a transport
// swap, not a second storage implementation. Must precede any DB open.
if (Platform.isLinux) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
// Before anything that stamps an Hlc or syncs: until this resolves, the
// device id falls back to the pre-migration constant.
await initSyncDeviceId();
@ -62,7 +75,12 @@ void main() async {
),
);
debugPrint('WorkoutApp: ${restored.reason}');
await HttpServerService.instance.start();
// Android-only transport: it exists so the PC can pull today's workout off
// the phone over LAN. On the PC itself it would bind the very port the PC
// scans, so it is skipped rather than serving the machine to itself.
if (!Platform.isLinux) {
await HttpServerService.instance.start();
}
runApp(const WorkoutApp());
}
// coverage:ignore-end
@ -95,6 +113,9 @@ class _WorkoutAppState extends State<WorkoutApp> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// The socket is never started on Linux (see main()), so there is nothing
// to stop or restart here either.
if (Platform.isLinux) return;
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.detached:

View File

@ -11,6 +11,7 @@ import 'package:workout_app/models/exercise.dart';
import 'package:workout_app/models/exercise_result.dart';
import 'package:workout_app/models/set_result.dart';
import 'package:workout_app/models/workout_session.dart';
import 'package:workout_app/services/lock_mode.dart';
import 'package:workout_app/services/progression_sync_service.dart';
import 'package:workout_app/services/storage_service.dart';
import 'package:workout_app/services/sync_service.dart';
@ -209,10 +210,11 @@ class _WorkoutScreenState extends State<WorkoutScreen> {
@override
Widget build(BuildContext context) {
return PopScope(
// Explicit `canPop: true` makes it clear this scope never blocks the back
// button — a future reader must not assume the default silently.
// ignore: avoid_redundant_argument_values
canPop: true,
// In lock mode the workout IS the lock: popping back to the home screen
// would leave a fullscreen window holding the X grab with no way to
// finish, so the route is pinned until the workout is finished or reset.
// Outside lock mode the back button behaves exactly as it always has.
canPop: !lockModeEnabled,
child: Scaffold(
appBar: _WorkoutAppBar(
title:

View File

@ -12,13 +12,19 @@
/// half. On Android there is no such file.
library;
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:workout_app/services/google_sign_in_backend.dart';
part 'firebase_backend_desktop.dart';
part 'firebase_backend_google.dart';
/// The shared `kuhy-syncs` project.
///
/// `databaseUrl` is the **regional** host. The plain `*.firebaseio.com` form
@ -42,14 +48,45 @@ const _secure = FlutterSecureStorage();
// coverage:ignore-start
/// The keystore-backed home for the Firebase refresh token.
SecureCredentialStore credentialStore() => SecureCredentialStore(
read: (key) => _secure.read(key: key),
write: (key, value) => _secure.write(key: key, value: value),
delete: (key) => _secure.delete(key: key),
);
///
/// On Linux the "keystore" is the shared 0600 file above rather than
/// libsecret, so the desktop build signs in with the same durable refresh
/// token the Python side already holds — the account password on this fleet
/// is stale, and only the refresh token still authenticates.
SecureCredentialStore credentialStore() {
if (Platform.isLinux) {
return SecureCredentialStore(
read: (key) async {
final file = _desktopCredentialFile();
if (!file.existsSync()) return null;
return file.readAsString();
},
write: (key, value) async {
final file = _desktopCredentialFile();
await file.parent.create(recursive: true);
await file.writeAsString(value);
// The refresh token is the secret worth protecting; keep it 0600 the
// way the Python half writes it.
await Process.run('chmod', ['600', file.path]);
},
delete: (key) async {
final file = _desktopCredentialFile();
if (file.existsSync()) await file.delete();
},
);
}
return SecureCredentialStore(
read: (key) => _secure.read(key: key),
write: (key, value) => _secure.write(key: key, value: value),
delete: (key) => _secure.delete(key: key),
);
}
/// Reads the per-device account, or null when sync has not been set up.
Future<FirebaseAccount?> loadAccount() async {
// Linux desktop reads the shared PC credential instead of the keystore,
// so a fresh desktop install is connected the moment it starts.
if (Platform.isLinux) return _accountFromDesktopConfig();
try {
return FirebaseAccount.tryParse(
await _secure.read(key: kFirebaseAccountKey),
@ -121,7 +158,15 @@ Future<FirebaseRestClient?> openFirebase() async {
// Passing '' would make firebaseClientFor treat it as a usable
// credential and sign in with it, which fails; null correctly
// means "no password on this device".
password: account.password.isEmpty ? null : account.password,
//
// On Linux the shared refresh token in ~/.config/screen_locker is the
// real credential and short-circuits sign-in before any password is
// consulted. The password in ~/.config/crdt-sync no longer authenticates
// on this fleet, so offering it would only turn a working token into an
// INVALID_LOGIN_CREDENTIALS failure.
password: (Platform.isLinux || account.password.isEmpty)
? null
: account.password,
// Deliberately NOT offering Google here. This path runs from background
// timers and, in some apps, before runApp -- offering it would let a
// non-interactive tick raise the OS account picker with no user action
@ -130,54 +175,6 @@ Future<FirebaseRestClient?> openFirebase() async {
);
}
/// Signs in with Google alone, for a device that has no account stored yet.
///
/// This is the one-tap path: [openFirebase] needs an account in the keystore
/// to know which email to use, but a fresh install has none. The Google token
/// carries the identity, so nothing needs to be typed.
///
/// The account is stored under the email **Firebase reports**, never one read
/// from the UI: a fresh install has no email anywhere on the device, so taking
/// it from a text field would persist an empty account and send the next
/// launch down the password path with ''.
///
/// Returns null when the user dismisses the picker; throws [FirebaseAuthError]
/// when Google succeeds but resolves to a uid the security rules do not pin,
/// which would otherwise authenticate fine and then be denied every read and
/// write with no other symptom.
Future<FirebaseRestClient?> openFirebaseWithGoogle({
Future<String?> Function()? tokenFetcher,
Future<void> Function(FirebaseAccount)? accountSaver,
http.Client? httpClient,
}) async {
final token = await (tokenFetcher ?? googleIdToken)();
if (token == null) return null;
final auth = FirebaseTokenProvider(
apiKey: kProject.apiKey,
store: credentialStore(),
httpClient: httpClient,
);
final email = await auth.signInWithGoogle(
idToken: token,
expectedUid: kSyncUid,
);
// Saved unconditionally, and deliberately not gated on `email`:
// `signInWithIdp` omits that field whenever the Google account hides it, and
// gating the write on it returned a working client while persisting nothing,
// so the next launch looked unconfigured and fell back to GitHub-only.
// The session itself is already durable here -- signInWithGoogle stored the
// refresh token -- and that token, not the address, is the credential.
await (accountSaver ?? saveAccount)(
FirebaseAccount(email: email ?? '', password: ''),
);
return FirebaseRestClient(databaseUrl: kProject.databaseUrl, auth: auth);
}
/// Whether this device can actually authenticate against Firebase.
///
/// True when either half of the state is present: the account marker
/// [loadAccount] reads, or a stored refresh token. The token is the half that
/// matters -- it is what signs requests -- so reporting the marker alone calls
/// a working device "not connected", which is exactly how a phone that was in
/// fact syncing looked broken.
Future<bool> isFirebaseConfigured() async {

View File

@ -0,0 +1,85 @@
part of 'firebase_backend.dart';
// The Linux-desktop credential edge. Kept beside the Android keystore path
// rather than in it, because the two answer the same questions from entirely
// different places: Android from the OS keystore, desktop from the 0600 files
// the Python half of this fleet already maintains.
//
// Every function here touches the filesystem or the environment, which the
// `flutter test` host has no meaningful version of -- the same reason the rest
// of firebase_backend.dart is excluded.
// coverage:ignore-start
/// Path of the shared desktop credential cache, matching the Python
/// `crdt_sync.credential_store_for("screen_locker")`.
///
/// Deliberately the SAME file the PC's own sync already uses: the fleet holds
/// one long-lived refresh token per app, and a second copy would drift out of
/// date the first time either half refreshed it.
File _desktopCredentialFile() => File(
p.join(
Platform.environment['HOME'] ?? '',
'.config',
'screen_locker',
'firebase_auth.json',
),
);
/// Reads the shared desktop credential from `~/.config/crdt-sync/`.
///
/// Linux desktop only. The PC half of this fleet (screen_locker, and the
/// `crdt_sync` Python library) already keeps the sync account there as
/// `firebase.json` + `password`, both mode 600, so the desktop build reads
/// that rather than standing up a second credential the user would have to
/// enter and keep in step. It also sidesteps `google_sign_in`, which has no
/// Linux implementation at all.
///
/// Returns null when the files are absent or malformed; the caller then
/// reports the device as not connected, exactly as an empty keystore would.
Future<FirebaseAccount?> _accountFromDesktopConfig() async {
try {
final home = Platform.environment['HOME'];
if (home == null || home.isEmpty) {
log(
'WorkoutApp: HOME is unset — cannot locate ~/.config/crdt-sync, so '
'this desktop is NOT connected to sync.',
level: 1000,
);
return null;
}
final dir = Directory(p.join(home, '.config', 'crdt-sync'));
final configFile = File(p.join(dir.path, 'firebase.json'));
final passwordFile = File(p.join(dir.path, 'password'));
if (!configFile.existsSync() || !passwordFile.existsSync()) {
log(
'WorkoutApp: no desktop sync credential at ${dir.path} '
'(need firebase.json + password) — this desktop is NOT connected to '
'sync, so progression and workouts will not reach any other device.',
level: 1000,
);
return null;
}
final config =
jsonDecode(await configFile.readAsString()) as Map<String, dynamic>;
final email = config['email'] as String?;
if (email == null || email.isEmpty) {
log(
'WorkoutApp: ${configFile.path} has no "email" key — cannot sign in.',
level: 1000,
);
return null;
}
final password = (await passwordFile.readAsString()).trim();
return FirebaseAccount(email: email, password: password);
} on Object catch (error) {
log(
'WorkoutApp: failed to read the desktop sync credential from '
'~/.config/crdt-sync ($error) — treating this desktop as NOT connected.',
level: 1000,
error: error,
);
return null;
}
}
// coverage:ignore-end

View File

@ -0,0 +1,57 @@
part of 'firebase_backend.dart';
// The interactive Google sign-in path. Separate from the rest of the backend
// because it is the only piece that can raise UI, and because it is dead code
// on Linux desktop -- google_sign_in ships no Linux implementation, so the
// desktop build authenticates from the shared refresh token instead.
// coverage:ignore-start
/// Signs in with Google alone, for a device that has no account stored yet.
///
/// This is the one-tap path: [openFirebase] needs an account in the keystore
/// to know which email to use, but a fresh install has none. The Google token
/// carries the identity, so nothing needs to be typed.
///
/// The account is stored under the email **Firebase reports**, never one read
/// from the UI: a fresh install has no email anywhere on the device, so taking
/// it from a text field would persist an empty account and send the next
/// launch down the password path with ''.
///
/// Returns null when the user dismisses the picker; throws [FirebaseAuthError]
/// when Google succeeds but resolves to a uid the security rules do not pin,
/// which would otherwise authenticate fine and then be denied every read and
/// write with no other symptom.
Future<FirebaseRestClient?> openFirebaseWithGoogle({
Future<String?> Function()? tokenFetcher,
Future<void> Function(FirebaseAccount)? accountSaver,
http.Client? httpClient,
}) async {
final token = await (tokenFetcher ?? googleIdToken)();
if (token == null) return null;
final auth = FirebaseTokenProvider(
apiKey: kProject.apiKey,
store: credentialStore(),
httpClient: httpClient,
);
final email = await auth.signInWithGoogle(
idToken: token,
expectedUid: kSyncUid,
);
// Saved unconditionally, and deliberately not gated on `email`:
// `signInWithIdp` omits that field whenever the Google account hides it, and
// gating the write on it returned a working client while persisting nothing,
// so the next launch looked unconfigured and fell back to GitHub-only.
// The session itself is already durable here -- signInWithGoogle stored the
// refresh token -- and that token, not the address, is the credential.
await (accountSaver ?? saveAccount)(
FirebaseAccount(email: email ?? '', password: ''),
);
return FirebaseRestClient(databaseUrl: kProject.databaseUrl, auth: auth);
}
/// Whether this device can actually authenticate against Firebase.
///
/// True when either half of the state is present: the account marker
/// [loadAccount] reads, or a stored refresh token. The token is the half that
/// matters -- it is what signs requests -- so reporting the marker alone calls
// coverage:ignore-end

View File

@ -0,0 +1,23 @@
/// Whether this process is acting as the screen lock.
///
/// The Linux runner takes the X grab and drops the window decorations when
/// `--lock-mode` is passed (see `linux/runner/my_application.cc`); this is the
/// Dart half, which removes the ways *out* of the app. Both halves read the
/// same argument so they can never disagree about which mode is live.
///
/// The distinction matters because a lock the user can dismiss is not a lock:
/// with enforcement armed, leaving the workout early has to be impossible
/// rather than merely discouraged.
library;
/// True when the process was started with `--lock-mode`.
///
/// Set once from `main()`; defaults to false so every test and the ordinary
/// windowed launch behave exactly as before.
bool lockModeEnabled = false;
/// The flag the runner and the Dart side agree on.
const String kLockModeFlag = '--lock-mode';
/// Returns whether [args] requests lock mode.
bool parseLockMode(List<String> args) => args.contains(kLockModeFlag);

View File

@ -3,8 +3,10 @@ library;
import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:workout_app/models/exercise.dart';
import 'package:workout_app/models/workout_plan.dart';
@ -103,7 +105,7 @@ class StorageService {
final dbPath =
_testDbPath ??
// coverage:ignore-start
p.join(await getDatabasesPath(), 'workout_app.db');
p.join(await _databaseDirectory(), 'workout_app.db');
// coverage:ignore-end
_db = await openDatabase(
dbPath,

View File

@ -93,6 +93,19 @@ extension StorageServiceSchema on StorageService {
}
}
// coverage:ignore-start
// Platform-channel / filesystem edge: Android answers from the sqflite
// plugin, Linux desktop has no such plugin and uses an XDG directory under
// the FFI factory wired up in main().
Future<String> _databaseDirectory() async {
if (Platform.isLinux) {
final dir = await getApplicationSupportDirectory();
return dir.path;
}
return getDatabasesPath();
}
// coverage:ignore-end
// ── Settings ───────────────────────────────────────────────────────────────
}

View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -0,0 +1,128 @@
# 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 "workout_app")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.kuhy.workout_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 "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>: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()

View File

@ -0,0 +1,88 @@
# 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}
)

View File

@ -0,0 +1,23 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_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);
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,27 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_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 $<TARGET_FILE:${plugin}_plugin>)
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)

View File

@ -0,0 +1,26 @@
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}")

View File

@ -0,0 +1,6 @@
#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);
}

View File

@ -0,0 +1,250 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#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)
// ---------------------------------------------------------------------------
// Lock mode (--lock-mode): this window IS the screen lock.
//
// The Python locker (screen_locker) still owns the gatelock arbiter claim and
// keeps holding the X grab while we start up, so our first grab attempts fail
// by design. We map fullscreen + override-redirect FIRST -- covering the
// screen before anything is released -- then print the ready line, and retry
// the grab every 200ms until the supervisor drops its own. Ordering matters:
// releasing before we are up would expose the live desktop for the whole of
// Flutter's cold start.
//
// There is no Dart API for override-redirect or an X11 grab, which is why
// this lives in the runner rather than in lib/.
// ---------------------------------------------------------------------------
// Retry cadence for the grab, matching gatelock's _DEFAULT_GRAB_RETRY_MS.
static const guint kGrabRetryMs = 200;
static gboolean lock_mode_enabled = FALSE;
static gboolean lock_grab_held = FALSE;
// Takes the exclusive seat grab. Returns TRUE while it should keep retrying.
static gboolean try_seat_grab(gpointer data) {
GtkWidget* window = GTK_WIDGET(data);
GdkWindow* gdk_window = gtk_widget_get_window(window);
if (gdk_window == nullptr) return G_SOURCE_CONTINUE; // not realized yet
GdkDisplay* display = gdk_window_get_display(gdk_window);
GdkSeat* seat = gdk_display_get_default_seat(display);
GdkGrabStatus status =
gdk_seat_grab(seat, gdk_window, GDK_SEAT_CAPABILITY_ALL,
TRUE, nullptr, nullptr, nullptr, nullptr);
if (status != GDK_GRAB_SUCCESS) {
// Expected until the supervisor releases. Never fatal: a lock that gave up
// here would leave the machine unlocked, which is the whole failure mode
// this design exists to prevent.
return G_SOURCE_CONTINUE;
}
lock_grab_held = TRUE;
g_print("WORKOUT_LOCK: grab acquired\n");
return G_SOURCE_REMOVE;
}
// Applies override-redirect + fullscreen, then starts the grab retry loop.
static void enter_lock_mode(GtkWindow* window) {
GtkWidget* widget = GTK_WIDGET(window);
gtk_window_set_decorated(window, FALSE);
gtk_window_set_deletable(window, FALSE);
gtk_window_set_keep_above(window, TRUE);
// An override-redirect window is invisible to the window manager, so
// gtk_window_fullscreen() -- which merely ASKS the WM for fullscreen -- does
// nothing here. Size and place it against the monitor geometry directly,
// otherwise the window keeps its default 1280x720 and leaves an uncovered
// strip of live desktop at the edge of a larger screen.
GdkDisplay* display = gtk_widget_get_display(widget);
GdkMonitor* monitor = gdk_display_get_primary_monitor(display);
if (monitor == nullptr) monitor = gdk_display_get_monitor(display, 0);
if (monitor != nullptr) {
GdkRectangle geometry;
gdk_monitor_get_geometry(monitor, &geometry);
gtk_window_move(window, geometry.x, geometry.y);
gtk_window_resize(window, geometry.width, geometry.height);
}
gtk_window_fullscreen(window);
// Realize (create the X window) without mapping it, so override-redirect is
// set on an unmapped window.
gtk_widget_realize(widget);
GdkWindow* gdk_window = gtk_widget_get_window(widget);
if (gdk_window != nullptr) {
// Bypass the WM entirely: i3 would otherwise re-tile or let the user
// move/close this window.
gdk_window_set_override_redirect(gdk_window, TRUE);
}
g_timeout_add(kGrabRetryMs, try_seat_grab, widget);
}
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view));
if (lock_mode_enabled) {
// Configure BEFORE the first map: toggling override-redirect on a window
// that is already mapped leaves it mapped but never repainting (a blank
// grey rectangle that still holds the grab -- the worst possible lock).
enter_lock_mode(GTK_WINDOW(toplevel));
gtk_widget_show(toplevel);
// The supervisor releases its own grab only after seeing this line, so the
// screen is already covered by us when it does. Flushed because stdout to
// a pipe is block-buffered and the handshake would otherwise deadlock.
g_print("WORKOUT_LOCK: ready\n");
fflush(stdout);
} else {
gtk_widget_show(toplevel);
}
}
// 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 = !lock_mode_enabled;
#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, "workout_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, "workout_app");
}
gtk_window_set_default_size(window, 1280, 720);
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);
// --lock-mode is handled here AND forwarded to Dart: the runner needs it
// for the grab, the Dart side for the no-exit UI.
for (gchar** arg = self->dart_entrypoint_arguments; *arg != nullptr; arg++) {
if (g_strcmp0(*arg, "--lock-mode") == 0) {
lock_mode_enabled = TRUE;
break;
}
}
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));
}

View File

@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
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_

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
sha256: be169cf6ac481e052c4538715d88841d567150dfe1df38aaec76461a4e7b39f2
url: "https://pub.dev"
source: hosted
version: "4.0.9"
version: "4.1.0"
args:
dependency: transitive
description:
@ -29,58 +29,58 @@ packages:
dependency: "direct main"
description:
name: audioplayers
sha256: "1d0c1b1f2095e59080e2d5046639096417a86687d89778da41b0c9a06d683dfd"
sha256: "2ba4bb2944baacbdd5372ff8254a8e7feb8c10d7739545e392f5605a8f618745"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.8.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
sha256: f5ff5b15620fbab8cb0849e9636c48e2b96c3f0f71723bbbe2ad3c761b205f05
url: "https://pub.dev"
source: hosted
version: "5.2.1"
version: "5.3.0"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
sha256: "1ca553add991384ecf421b9569da850f3ab2472ffb83f6970b0416365abc51be"
url: "https://pub.dev"
source: hosted
version: "6.4.0"
version: "6.5.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
sha256: "15178b726b7cdee5364d0463c8d445630c4e0fb7d26612b73c767e7d25de9417"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
version: "4.3.0"
audioplayers_platform_interface:
dependency: "direct dev"
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
sha256: "765f6f0e6dca55cb471c9483fc77700564b3484d19198aca4ebb5147c6c85acb"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
version: "7.2.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545"
sha256: ae1e0103c865a03e273f6d13d97b93f5595eac09915729cd5e37ef96e2857319
url: "https://pub.dev"
source: hosted
version: "5.2.1"
version: "5.3.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a"
sha256: a70ae82bba2dfcb6eb03dd4815d737a2d46d33ea5a96a03f535cfcaac490e413
url: "https://pub.dev"
source: hosted
version: "4.3.1"
version: "4.4.1"
boolean_selector:
dependency: transitive
description:
@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: code_assets
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
collection:
dependency: transitive
description:
@ -158,10 +158,10 @@ packages:
dependency: transitive
description:
name: device_info_plus
sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65"
sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442"
url: "https://pub.dev"
source: hosted
version: "13.1.0"
version: "13.2.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -251,18 +251,18 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5
sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.3"
flutter_secure_storage_web:
dependency: transitive
description:
@ -349,10 +349,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.2"
http:
dependency: "direct main"
description:
@ -373,26 +373,34 @@ packages:
dependency: transitive
description:
name: image
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
url: "https://pub.dev"
source: hosted
version: "4.9.1"
version: "4.9.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.3"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
jni_util:
dependency: transitive
description:
name: jni_util
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
json_annotation:
dependency: transitive
description:
@ -461,34 +469,34 @@ packages:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.18.3"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: f59351d28f49520cd3a74eb1f41c5f19ae15e53c65a3231d14af672e46510a96
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev"
source: hosted
version: "0.19.1"
version: "0.19.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
url: "https://pub.dev"
source: hosted
version: "9.4.1"
version: "9.5.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "3.0.0"
path:
dependency: "direct main"
description:
@ -501,10 +509,10 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
@ -525,18 +533,18 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
@ -549,10 +557,10 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: ca045d03615023c08ccdb297aad46a9198193666039ddd36d4d85fd0b1864b98
sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
url: "https://pub.dev"
source: hosted
version: "12.0.2"
version: "12.0.3"
permission_handler_android:
dependency: transitive
description:
@ -565,42 +573,34 @@ packages:
dependency: transitive
description:
name: permission_handler_apple
sha256: "447c18bc3c5fdea5c3039f042b2b365fd51e3634f5f6e269ed22c1f00071addc"
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
url: "https://pub.dev"
source: hosted
version: "9.4.8"
version: "9.6.1"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
version: "0.1.4+1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23
url: "https://pub.dev"
source: hosted
version: "4.3.0"
version: "4.4.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
version: "0.2.2"
platform:
dependency: transitive
description:
@ -621,10 +621,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.5.2"
pub_semver:
dependency: transitive
description:
@ -653,10 +653,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
url: "https://pub.dev"
source: hosted
version: "2.4.23"
version: "2.4.27"
shared_preferences_foundation:
dependency: transitive
description:
@ -714,58 +714,58 @@ packages:
dependency: "direct main"
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
url: "https://pub.dev"
source: hosted
version: "2.4.2+1"
version: "2.4.3"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
version: "2.4.3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: cce558075afe2a83f3fd7fc123acd6b090683e4f23910d44fbb31ecd7800b014
sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
url: "https://pub.dev"
source: hosted
version: "2.5.9"
version: "2.5.11"
sqflite_common_ffi:
dependency: "direct dev"
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: "3ddad0ec96ad411d5fea45b4912c3cd5743436c9e1890c26a6e688a32d901cae"
sha256: d5564f1308cafbf064be498f2beb1e993e742985a7cca9e2e228d6cbccd84a05
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.2+1"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.3+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "9488c7d2cdb1091c91cacf7e207cff81b28bff8e366f042bad3afe7d34afe189"
sha256: "4c7fe79840389aaeaf05fd093f795b631b5a98e2bd28d54e555c100f4a9c7a1c"
url: "https://pub.dev"
source: hosted
version: "3.3.2"
version: "3.5.2"
stack_trace:
dependency: transitive
description:
@ -803,10 +803,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153"
sha256: "3a7b5d17422dd0f8d5c6c14feaa5a1c65638b9455f871a96f08437562c046931"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.4.1+2"
term_glyph:
dependency: transitive
description:
@ -907,26 +907,26 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.0"
very_good_analysis:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0
sha256: "481af67ab5877af20325251dc215a4ebac7666a1c8cf09198ffd457bc612b33d"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
version: "10.3.0"
vibration:
dependency: "direct main"
description:
name: vibration
sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87"
sha256: c6c25eb7acadfd231925a60004736443cbf63f1eb5740d6dd83a1bc09cf00f6d
url: "https://pub.dev"
source: hosted
version: "3.1.8"
version: "3.2.0"
vibration_platform_interface:
dependency: transitive
description:
@ -939,10 +939,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
version: "15.3.0"
web:
dependency: transitive
description:
@ -955,10 +955,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.4.0"
win32_registry:
dependency: transitive
description:
@ -975,14 +975,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
yaml:
dependency: transitive
description:

View File

@ -27,6 +27,9 @@ dependencies:
permission_handler: ^12.0.0
shared_preferences: ^2.5.3
sqflite: ^2.4.2
# Linux desktop has no sqflite plugin; the FFI factory drives the same
# schema (and already backed the test suite before the desktop target).
sqflite_common_ffi: ^2.4.1
sync_settings_ui:
git:
url: https://github.com/kuhyx/utils
@ -44,7 +47,6 @@ dev_dependencies:
flutter_lints: ^6.0.0
flutter_test:
sdk: flutter
sqflite_common_ffi: ^2.4.1
very_good_analysis: ^10.2.0
flutter: