diff --git a/lib/components/home/syncing_button.dart b/lib/components/home/syncing_button.dart index bfd58b360..64e313482 100644 --- a/lib/components/home/syncing_button.dart +++ b/lib/components/home/syncing_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:saber/components/notifs/snackbar.dart'; import 'package:saber/components/theming/adaptive_icon.dart'; import 'package:saber/data/nextcloud/saber_syncer.dart'; import 'package:saber/data/prefs.dart'; @@ -46,6 +47,11 @@ class _SyncingButtonState extends State { if (mounted) setState(() {}); } + void _snackBarSyncOnlyOverWifi() { + if (!mounted) return; + SnackBarNotification.show(context, message: 'Wifi is not connected, disable "sync only over wifi" in settings to use mobile data.'); // fixme + } + /// Returns a value between 0-1 representing the progress of the sync, /// or null if we're still refreshing. double? getPercentage() { @@ -60,9 +66,14 @@ class _SyncingButtonState extends State { return filesTransferred / (filesTransferred + numPending); } - void onPressed() { + void onPressed() async { assert(Prefs.loggedIn); + if (!(await SaberSyncInterface.shouldSync())) { + _snackBarSyncOnlyOverWifi(); + return; + } + // Don't refresh if we're already refreshing. if (syncer.downloader.isRefreshing) return; diff --git a/lib/components/notifs/snackbar.dart b/lib/components/notifs/snackbar.dart new file mode 100644 index 000000000..0837f8e0d --- /dev/null +++ b/lib/components/notifs/snackbar.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class SnackBarNotification extends StatelessWidget { + final String message; + + const SnackBarNotification({ + super.key, + required this.message, + }); + + @override + Widget build(BuildContext context) { + return SnackBar( + content: Text(message), + ); + } + + static void show(BuildContext context, {required String message}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + ), + ); + } +} diff --git a/lib/components/settings/nextcloud_profile.dart b/lib/components/settings/nextcloud_profile.dart index 653633c8a..dafa00ca8 100644 --- a/lib/components/settings/nextcloud_profile.dart +++ b/lib/components/settings/nextcloud_profile.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:nextcloud/provisioning_api.dart'; +import 'package:saber/components/notifs/snackbar.dart'; import 'package:saber/components/theming/adaptive_icon.dart'; import 'package:saber/data/file_manager/file_manager.dart'; import 'package:saber/data/nextcloud/nextcloud_client_extension.dart'; @@ -52,6 +53,19 @@ class _NextcloudProfileState extends State { if (mounted) setState(() {}); } + void _resyncEverything() async { + Prefs.fileSyncResyncEverythingDate.value = DateTime.now(); + final allFiles = await FileManager.getAllFiles(includeExtensions: true, includeAssets: true); + for (final file in allFiles) { + syncer.uploader.enqueueRel(file); + } + } + + void _snackBarSyncOnlyOverWifi() { + if (!mounted) return; + SnackBarNotification.show(context, message: 'Wifi is not connected, disable "sync only over wifi" in settings to use mobile data.'); // fixme + } + @override Widget build(BuildContext context) { final loginStep = @@ -71,6 +85,7 @@ class _NextcloudProfileState extends State { }; var colorScheme = Theme.of(context).colorScheme; + return ListTile( onTap: () => context.push(RoutePaths.login), leading: ValueListenableBuilder( @@ -140,12 +155,11 @@ class _NextcloudProfileState extends State { ), tooltip: t.settings.resyncEverything, onPressed: () async { - Prefs.fileSyncResyncEverythingDate.value = DateTime.now(); - final allFiles = await FileManager.getAllFiles( - includeExtensions: true, includeAssets: true); - for (final file in allFiles) { - syncer.uploader.enqueueRel(file); + if (!(await SaberSyncInterface.shouldSync())) { + _snackBarSyncOnlyOverWifi(); + return; } + _resyncEverything(); }, ), ], diff --git a/lib/data/nextcloud/saber_syncer.dart b/lib/data/nextcloud/saber_syncer.dart index 1ca8315ab..8784df32e 100644 --- a/lib/data/nextcloud/saber_syncer.dart +++ b/lib/data/nextcloud/saber_syncer.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:abstract_sync/abstract_sync.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypt/encrypt.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; @@ -22,6 +23,16 @@ class SaberSyncInterface extends AbstractSyncInterface { const SaberSyncInterface(); + static Future shouldSync() async { + if (Prefs.onlySyncOverWifi.value) { + final List connRes = await Connectivity().checkConnectivity(); + if (!connRes.contains(ConnectivityResult.wifi)) { + return false; + } + } + return true; + } + static final log = Logger('SaberSyncInterface'); @override diff --git a/lib/data/prefs.dart b/lib/data/prefs.dart index bd68bd968..925ce0deb 100644 --- a/lib/data/prefs.dart +++ b/lib/data/prefs.dart @@ -62,6 +62,7 @@ abstract class Prefs { static late final PlainPref pfp; static late final PlainPref syncInBackground; + static late final PlainPref onlySyncOverWifi; static late final PlainPref appTheme; @@ -168,6 +169,7 @@ abstract class Prefs { pfp = PlainPref('pfp', null); syncInBackground = PlainPref('syncInBackground', true); + onlySyncOverWifi = PlainPref('onlySyncOverWifi', true); appTheme = PlainPref('appTheme', ThemeMode.system); platform = PlainPref('platform', defaultTargetPlatform); diff --git a/lib/main_common.dart b/lib/main_common.dart index fb3affc93..b1351f8cc 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -159,7 +159,10 @@ void setupBackgroundSync() { } @pragma('vm:entry-point') -void doBackgroundSync() { +void doBackgroundSync() async { + if (!(await SaberSyncInterface.shouldSync())) { + return; // FIXME: should we warn user in this case? + } Workmanager().executeTask((_, __) async { StrokeOptionsExtension.setDefaults(); Prefs.init(); diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index b53bcd237..503ee0fa3 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -21,6 +21,7 @@ import 'package:saber/components/canvas/canvas_preview.dart'; import 'package:saber/components/canvas/image/editor_image.dart'; import 'package:saber/components/canvas/save_indicator.dart'; import 'package:saber/components/navbar/responsive_navbar.dart'; +import 'package:saber/components/notifs/snackbar.dart'; import 'package:saber/components/theming/adaptive_alert_dialog.dart'; import 'package:saber/components/theming/adaptive_icon.dart'; import 'package:saber/components/theming/dynamic_material_app.dart'; @@ -1722,9 +1723,7 @@ class EditorState extends State { void snackBarNeedsToSaveBeforeExiting() { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(t.editor.needsToSaveBeforeExiting), - )); + SnackBarNotification.show(context, message: t.editor.needsToSaveBeforeExiting); } Widget bottomSheet(BuildContext context) { diff --git a/lib/pages/home/settings.dart b/lib/pages/home/settings.dart index a934940c3..47a5065ca 100644 --- a/lib/pages/home/settings.dart +++ b/lib/pages/home/settings.dart @@ -329,6 +329,15 @@ class _SettingsPageState extends State { }, pref: Prefs.hyperlegibleFont, ), + SettingsSwitch( + title: 'Sync notes only over WiFi', + subtitle: 'If disabled mobile data may be used to sync notes', + iconBuilder: (i) => switch (Prefs.platform.value) { + TargetPlatform.iOS || TargetPlatform.macOS => Icons.wifi_rounded, + _ => Icons.wifi_sharp, + }, + pref: Prefs.onlySyncOverWifi, + ), SettingsSubtitle(subtitle: t.settings.prefCategories.writing), SettingsSwitch( title: t.settings.prefLabels.preferGreyscale, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a37a01931..c10df1d54 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import audioplayers_darwin +import connectivity_plus import desktop_webview_window import device_info_plus import dynamic_color @@ -14,7 +15,8 @@ import flutter_web_auth_2 import irondash_engine_context import path_provider_foundation import printing -import screen_retriever +import quill_native_bridge_macos +import screen_retriever_macos import share_plus import shared_preferences_foundation import super_native_extensions @@ -24,6 +26,7 @@ import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) @@ -32,7 +35,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) - ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4c8ba515d..25ae91500 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -917,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" one_dollar_unistroke_recognizer: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f9e1a2636..ca0be541a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: args: ^2.5.0 flutter_web_auth_2: ^4.0.0 + connectivity_plus: ^6.0.5 pdfrx: ^1.0.85 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index df63b981f..02e7ce019 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,13 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include #include #include #include -#include +#include #include #include #include @@ -23,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DesktopWebviewWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( @@ -35,8 +38,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); PrintingPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PrintingPlugin")); - ScreenRetrieverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); SuperNativeExtensionsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4b5296f67..6b9caaa7f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,13 +4,14 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows + connectivity_plus desktop_webview_window dynamic_color flutter_secure_storage_windows irondash_engine_context permission_handler_windows printing - screen_retriever + screen_retriever_windows share_plus super_native_extensions url_launcher_windows