diff --git a/stronglift_replacement/workout_app/.metadata b/stronglift_replacement/workout_app/.metadata index cda28f3..9be373a 100644 --- a/stronglift_replacement/workout_app/.metadata +++ b/stronglift_replacement/workout_app/.metadata @@ -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 diff --git a/stronglift_replacement/workout_app/lib/main.dart b/stronglift_replacement/workout_app/lib/main.dart index 1418ea3..afc909b 100644 --- a/stronglift_replacement/workout_app/lib/main.dart +++ b/stronglift_replacement/workout_app/lib/main.dart @@ -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 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 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: diff --git a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart index 30d0f68..2674123 100644 --- a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart @@ -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 { @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: diff --git a/stronglift_replacement/workout_app/lib/services/firebase_backend.dart b/stronglift_replacement/workout_app/lib/services/firebase_backend.dart index 3af072b..79ea7dd 100644 --- a/stronglift_replacement/workout_app/lib/services/firebase_backend.dart +++ b/stronglift_replacement/workout_app/lib/services/firebase_backend.dart @@ -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 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 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 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 openFirebaseWithGoogle({ - Future Function()? tokenFetcher, - Future 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 isFirebaseConfigured() async { diff --git a/stronglift_replacement/workout_app/lib/services/firebase_backend_desktop.dart b/stronglift_replacement/workout_app/lib/services/firebase_backend_desktop.dart new file mode 100644 index 0000000..d40ee35 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/services/firebase_backend_desktop.dart @@ -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 _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; + 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 diff --git a/stronglift_replacement/workout_app/lib/services/firebase_backend_google.dart b/stronglift_replacement/workout_app/lib/services/firebase_backend_google.dart new file mode 100644 index 0000000..b46f762 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/services/firebase_backend_google.dart @@ -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 openFirebaseWithGoogle({ + Future Function()? tokenFetcher, + Future 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 diff --git a/stronglift_replacement/workout_app/lib/services/lock_mode.dart b/stronglift_replacement/workout_app/lib/services/lock_mode.dart new file mode 100644 index 0000000..40d5f05 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/services/lock_mode.dart @@ -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 args) => args.contains(kLockModeFlag); diff --git a/stronglift_replacement/workout_app/lib/services/storage_service.dart b/stronglift_replacement/workout_app/lib/services/storage_service.dart index bab27fd..98b9929 100644 --- a/stronglift_replacement/workout_app/lib/services/storage_service.dart +++ b/stronglift_replacement/workout_app/lib/services/storage_service.dart @@ -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, diff --git a/stronglift_replacement/workout_app/lib/services/storage_service_schema.dart b/stronglift_replacement/workout_app/lib/services/storage_service_schema.dart index 45383d9..7ebb7e2 100644 --- a/stronglift_replacement/workout_app/lib/services/storage_service_schema.dart +++ b/stronglift_replacement/workout_app/lib/services/storage_service_schema.dart @@ -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 _databaseDirectory() async { + if (Platform.isLinux) { + final dir = await getApplicationSupportDirectory(); + return dir.path; + } + return getDatabasesPath(); + } + // coverage:ignore-end + // ── Settings ─────────────────────────────────────────────────────────────── } diff --git a/stronglift_replacement/workout_app/linux/.gitignore b/stronglift_replacement/workout_app/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/stronglift_replacement/workout_app/linux/CMakeLists.txt b/stronglift_replacement/workout_app/linux/CMakeLists.txt new file mode 100644 index 0000000..1d33329 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/CMakeLists.txt @@ -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 "$<$>:-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/stronglift_replacement/workout_app/linux/flutter/CMakeLists.txt b/stronglift_replacement/workout_app/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/flutter/CMakeLists.txt @@ -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} +) diff --git a/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.cc b/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..ef9ba8a --- /dev/null +++ b/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// 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) 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); +} diff --git a/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.h b/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/stronglift_replacement/workout_app/linux/flutter/generated_plugins.cmake b/stronglift_replacement/workout_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..69de96a --- /dev/null +++ b/stronglift_replacement/workout_app/linux/flutter/generated_plugins.cmake @@ -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 $) + 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/stronglift_replacement/workout_app/linux/runner/CMakeLists.txt b/stronglift_replacement/workout_app/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/stronglift_replacement/workout_app/linux/runner/CMakeLists.txt @@ -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}") diff --git a/stronglift_replacement/workout_app/linux/runner/main.cc b/stronglift_replacement/workout_app/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/runner/main.cc @@ -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); +} diff --git a/stronglift_replacement/workout_app/linux/runner/my_application.cc b/stronglift_replacement/workout_app/linux/runner/my_application.cc new file mode 100644 index 0000000..80f490c --- /dev/null +++ b/stronglift_replacement/workout_app/linux/runner/my_application.cc @@ -0,0 +1,250 @@ +#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) + +// --------------------------------------------------------------------------- +// 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)); +} diff --git a/stronglift_replacement/workout_app/linux/runner/my_application.h b/stronglift_replacement/workout_app/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/stronglift_replacement/workout_app/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#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/stronglift_replacement/workout_app/pubspec.lock b/stronglift_replacement/workout_app/pubspec.lock index 8cc899b..b3c5dcf 100644 --- a/stronglift_replacement/workout_app/pubspec.lock +++ b/stronglift_replacement/workout_app/pubspec.lock @@ -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: diff --git a/stronglift_replacement/workout_app/pubspec.yaml b/stronglift_replacement/workout_app/pubspec.yaml index 890df84..6bd12ed 100644 --- a/stronglift_replacement/workout_app/pubspec.yaml +++ b/stronglift_replacement/workout_app/pubspec.yaml @@ -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: