sync initial
This commit is contained in:
@@ -179,6 +179,19 @@
|
|||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="eu.weblibre.flutter_mozilla_components.activities.AuthCustomTabActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:autoRemoveFromRecents="false"
|
||||||
|
android:theme="@style/ExternalAppBrowserTheme"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="eu.weblibre.flutter_mozilla_components.activities.AuthIntentReceiverActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:theme="@style/AddonsActivityTheme"
|
android:theme="@style/AddonsActivityTheme"
|
||||||
android:name="eu.weblibre.flutter_mozilla_components.addons.AddonsActivity"
|
android:name="eu.weblibre.flutter_mozilla_components.addons.AddonsActivity"
|
||||||
|
|||||||
@@ -20,9 +20,26 @@
|
|||||||
package eu.weblibre.gecko
|
package eu.weblibre.gecko
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import eu.weblibre.flutter_mozilla_components.ActiveProfile
|
||||||
|
import eu.weblibre.flutter_mozilla_components.MegazordSetup
|
||||||
|
|
||||||
class MyApplication : Application() {
|
class MyApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
MegazordSetup.setupEarlyMainProcess()
|
||||||
|
|
||||||
|
// Resolve active profile EARLY so cold-start WorkManager workers
|
||||||
|
// get profile-prefixed SharedPreferences
|
||||||
|
ActiveProfile.resolveFromDisk(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
|
||||||
|
val pfx = ActiveProfile.prefix
|
||||||
|
if (pfx != null && name in ActiveProfile.FXA_SHARED_PREFERENCE_NAMES) {
|
||||||
|
return super.getSharedPreferences("${pfx}_$name", mode)
|
||||||
|
}
|
||||||
|
return super.getSharedPreferences(name, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ part 'format.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class Format extends _$Format {
|
class Format extends _$Format {
|
||||||
String fullDateTimeWithTimezone(DateTime date) {
|
String fullDateTime(DateTime date) {
|
||||||
final pattern = DateFormat('yMMMMd').addPattern('Hm');
|
final pattern = DateFormat('yMMMMd').addPattern('Hm');
|
||||||
|
|
||||||
return pattern.format(date);
|
return pattern.format(date);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class FormatProvider extends $AsyncNotifierProvider<Format, void> {
|
|||||||
Format create() => Format();
|
Format create() => Format();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$formatHash() => r'fe7fcdd19b512784a36f3838c57701030475e429';
|
String _$formatHash() => r'f132aabb20bf4df77d771d6c866bc413c00bd9ff';
|
||||||
|
|
||||||
abstract class _$Format extends $AsyncNotifier<void> {
|
abstract class _$Format extends $AsyncNotifier<void> {
|
||||||
FutureOr<void> build();
|
FutureOr<void> build();
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_se
|
|||||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||||
|
import 'package:weblibre/features/sync/presentation/screens/sync_settings.dart';
|
||||||
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
||||||
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
||||||
|
|||||||
@@ -1427,6 +1427,11 @@ RouteBase get $settingsRoute => GoRouteData.$route(
|
|||||||
name: 'ErrorLogsRoute',
|
name: 'ErrorLogsRoute',
|
||||||
factory: $ErrorLogsRoute._fromState,
|
factory: $ErrorLogsRoute._fromState,
|
||||||
),
|
),
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'sync',
|
||||||
|
name: 'SyncSettingsRoute',
|
||||||
|
factory: $SyncSettingsRoute._fromState,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1791,6 +1796,27 @@ mixin $ErrorLogsRoute on GoRouteData {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin $SyncSettingsRoute on GoRouteData {
|
||||||
|
static SyncSettingsRoute _fromState(GoRouterState state) =>
|
||||||
|
SyncSettingsRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get location => GoRouteData.$location('/settings/sync');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
RouteBase get $torProxyRoute => GoRouteData.$route(
|
RouteBase get $torProxyRoute => GoRouteData.$route(
|
||||||
path: '/tor',
|
path: '/tor',
|
||||||
name: 'TorProxyRoute',
|
name: 'TorProxyRoute',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ part of 'routes.dart';
|
|||||||
path: 'custom_tracking_protection',
|
path: 'custom_tracking_protection',
|
||||||
),
|
),
|
||||||
TypedGoRoute<ErrorLogsRoute>(name: 'ErrorLogsRoute', path: 'error_logs'),
|
TypedGoRoute<ErrorLogsRoute>(name: 'ErrorLogsRoute', path: 'error_logs'),
|
||||||
|
TypedGoRoute<SyncSettingsRoute>(name: 'SyncSettingsRoute', path: 'sync'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
||||||
@@ -214,3 +215,10 @@ class CustomTrackingProtectionRoute extends GoRouteData
|
|||||||
return const CustomTrackingProtectionScreen();
|
return const CustomTrackingProtectionScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SyncSettingsRoute extends GoRouteData with $SyncSettingsRoute {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return const SyncSettingsScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||||
@@ -55,6 +55,7 @@ import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_
|
|||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/utils/move_to_background.dart';
|
import 'package:weblibre/utils/move_to_background.dart';
|
||||||
@@ -303,6 +304,44 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useOnAppLifecycleStateChange((previous, current) {
|
||||||
|
switch (current) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
if (current != AppLifecycleState.resumed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ref.read(syncIsAuthenticatedProvider)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(() async {
|
||||||
|
try {
|
||||||
|
final openedTabs = await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.pollIncomingTabsAndOpen();
|
||||||
|
|
||||||
|
if (openedTabs > 0 && context.mounted) {
|
||||||
|
ui_helper.showOpenedTabsFromAnotherDeviceMessage(
|
||||||
|
context,
|
||||||
|
openedTabs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
logger.e(
|
||||||
|
'Failed polling incoming sync tabs on resume',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.hidden:
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
final pointerMoveEventsController = useStreamController<Offset>();
|
final pointerMoveEventsController = useStreamController<Offset>();
|
||||||
|
|
||||||
// Watch sheet state for rendering in Stack
|
// Watch sheet state for rendering in Stack
|
||||||
@@ -317,8 +356,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible)
|
// Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible)
|
||||||
// The controller handles loading-start show internally via ref.listen on isLoading
|
// The controller handles loading-start show internally via ref.listen on isLoading
|
||||||
final effectiveAppBarVisible =
|
final effectiveAppBarVisible = toolbarState == ToolbarVisibility.visible;
|
||||||
toolbarState == ToolbarVisibility.visible;
|
|
||||||
final topToolbarVisible =
|
final topToolbarVisible =
|
||||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
||||||
final bottomToolbarVisible =
|
final bottomToolbarVisible =
|
||||||
@@ -1020,6 +1058,16 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
|||||||
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
||||||
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
||||||
|
|
||||||
|
final isSyncedScope = ref.watch(
|
||||||
|
effectiveTabsTrayScopeProvider.select(
|
||||||
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final effectiveTabsViewMode = isSyncedScope
|
||||||
|
? TabsViewMode.list
|
||||||
|
: tabsViewMode;
|
||||||
|
|
||||||
final draggableScrollableController = useDraggableScrollableController(
|
final draggableScrollableController = useDraggableScrollableController(
|
||||||
keys: [tabsReorderable],
|
keys: [tabsReorderable],
|
||||||
);
|
);
|
||||||
@@ -1037,7 +1085,7 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
|||||||
topRight: Radius.circular(28),
|
topRight: Radius.circular(28),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: switch (tabsViewMode) {
|
child: switch (effectiveTabsViewMode) {
|
||||||
TabsViewMode.list => ViewTabListWidget(
|
TabsViewMode.list => ViewTabListWidget(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
showNewTabFab: true,
|
showNewTabFab: true,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
|
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
|
||||||
|
|
||||||
@@ -37,6 +38,16 @@ class TabViewScreen extends HookConsumerWidget {
|
|||||||
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
||||||
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
||||||
|
|
||||||
|
final isSyncedScope = ref.watch(
|
||||||
|
effectiveTabsTrayScopeProvider.select(
|
||||||
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final effectiveTabsViewMode = isSyncedScope
|
||||||
|
? TabsViewMode.list
|
||||||
|
: tabsViewMode;
|
||||||
|
|
||||||
final scrollController = useScrollController(keys: [tabsReorderable]);
|
final scrollController = useScrollController(keys: [tabsReorderable]);
|
||||||
|
|
||||||
// Track FAB visibility based on scroll direction
|
// Track FAB visibility based on scroll direction
|
||||||
@@ -45,7 +56,7 @@ class TabViewScreen extends HookConsumerWidget {
|
|||||||
return Dialog.fullscreen(
|
return Dialog.fullscreen(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: switch (tabsViewMode) {
|
child: switch (effectiveTabsViewMode) {
|
||||||
TabsViewMode.list => ViewTabListWidget(
|
TabsViewMode.list => ViewTabListWidget(
|
||||||
key: ValueKey(tabsReorderable),
|
key: ValueKey(tabsReorderable),
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
@@ -73,31 +84,35 @@ class TabViewScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: AnimatedSlide(
|
floatingActionButton: isSyncedScope
|
||||||
duration: const Duration(milliseconds: 200),
|
? null
|
||||||
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
: AnimatedSlide(
|
||||||
curve: Curves.easeInOut,
|
duration: const Duration(milliseconds: 200),
|
||||||
child: AnimatedOpacity(
|
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
||||||
duration: const Duration(milliseconds: 200),
|
curve: Curves.easeInOut,
|
||||||
opacity: isFabVisible.value ? 1.0 : 0.0,
|
child: AnimatedOpacity(
|
||||||
child: FloatingActionButton(
|
duration: const Duration(milliseconds: 200),
|
||||||
onPressed: () async {
|
opacity: isFabVisible.value ? 1.0 : 0.0,
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
child: FloatingActionButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final settings = ref.read(
|
||||||
|
generalSettingsWithDefaultsProvider,
|
||||||
|
);
|
||||||
|
|
||||||
await SearchRoute(
|
await SearchRoute(
|
||||||
tabType:
|
tabType:
|
||||||
ref.read(selectedTabTypeProvider) ??
|
ref.read(selectedTabTypeProvider) ??
|
||||||
settings.defaultCreateTabType,
|
settings.defaultCreateTabType,
|
||||||
).push(context);
|
).push(context);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
const BrowserRoute().go(context);
|
const BrowserRoute().go(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -670,6 +670,7 @@ class ShareMenuButton extends HookConsumerWidget {
|
|||||||
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShareMenuItemButton(selectedTabId: selectedTabId),
|
ShareMenuItemButton(selectedTabId: selectedTabId),
|
||||||
|
SendTabToDeviceMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
||||||
],
|
],
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
|
|||||||
+79
-1
@@ -18,6 +18,8 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -31,12 +33,15 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart';
|
import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart';
|
||||||
import 'package:weblibre/features/user/domain/providers.dart';
|
import 'package:weblibre/features/user/domain/providers.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
||||||
import 'package:weblibre/utils/exit_app.dart';
|
import 'package:weblibre/utils/exit_app.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
/// Navigation drawer for the browser screen.
|
/// Navigation drawer for the browser screen.
|
||||||
/// Contains all navigation destinations and settings.
|
/// Contains all navigation destinations and settings.
|
||||||
@@ -52,7 +57,7 @@ class BrowserNavigationDrawer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Profile Header
|
// Profile Header
|
||||||
_ProfileHeader(),
|
_ProfileHeader(),
|
||||||
|
_SyncTile(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
|
||||||
// Section 1: Tools & Configuration
|
// Section 1: Tools & Configuration
|
||||||
@@ -327,3 +332,76 @@ class _ExtensionsSection extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync tile widget shown in navigation drawer when sync is active.
|
||||||
|
/// Displays sync status and allows manual sync trigger.
|
||||||
|
class _SyncTile extends HookConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||||
|
|
||||||
|
// Only show if sync is active
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final syncInfo = ref.watch(
|
||||||
|
syncRepositoryProvider.select((value) => value.value?.account),
|
||||||
|
);
|
||||||
|
|
||||||
|
final syncStarted = ref.watch(
|
||||||
|
syncEventProvider.select(
|
||||||
|
(value) => value.isLoading || value.value?.$1 == SyncEvent.started,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final isSyncing = syncStarted || syncInfo?.syncing == true;
|
||||||
|
|
||||||
|
final controller = useAnimationController(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (isSyncing) {
|
||||||
|
unawaited(controller.repeat());
|
||||||
|
} else {
|
||||||
|
controller.stop();
|
||||||
|
controller.reset();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [isSyncing]);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: RotationTransition(
|
||||||
|
turns: Tween<double>(begin: 0, end: -1).animate(controller),
|
||||||
|
child: const Icon(Icons.sync),
|
||||||
|
),
|
||||||
|
title: const Text('Sync Now'),
|
||||||
|
onTap: () async {
|
||||||
|
await ref.read(syncRepositoryProvider.notifier).syncNow();
|
||||||
|
|
||||||
|
final openedTabs = await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.pollIncomingTabsAndOpen();
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
if (openedTabs > 0) {
|
||||||
|
ui_helper.showOpenedTabsFromAnotherDeviceMessage(
|
||||||
|
context,
|
||||||
|
openedTabs,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Synchronization complete',
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+97
@@ -26,13 +26,16 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class ShareMenuItemButton extends HookConsumerWidget {
|
class ShareMenuItemButton extends HookConsumerWidget {
|
||||||
const ShareMenuItemButton({super.key, required this.selectedTabId});
|
const ShareMenuItemButton({super.key, required this.selectedTabId});
|
||||||
@@ -287,3 +290,97 @@ class CopyAddressMenuItemButton extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SendTabToDeviceMenuItemButton extends HookConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const SendTabToDeviceMenuItemButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (selectedTabId == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||||
|
final devices = ref.watch(syncDevicesProvider);
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: devices.isLoading && devices.value == null,
|
||||||
|
child: SubmenuButton(
|
||||||
|
leadingIcon: const Icon(Icons.send_outlined),
|
||||||
|
menuChildren: devices.when(
|
||||||
|
data: (deviceList) {
|
||||||
|
final targets = deviceList
|
||||||
|
.where((device) => !device.isCurrentDevice && device.canSendTab)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
return const [MenuItemButton(child: Text('No target devices'))];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
|
.map((device) {
|
||||||
|
return MenuItemButton(
|
||||||
|
closeOnActivate: false,
|
||||||
|
leadingIcon: const Icon(Icons.devices_other),
|
||||||
|
child: Text(device.displayName),
|
||||||
|
onPressed: () async {
|
||||||
|
final tabState = ref.read(
|
||||||
|
tabStateProvider(selectedTabId),
|
||||||
|
);
|
||||||
|
if (tabState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final title = tabState.title.isNotEmpty
|
||||||
|
? tabState.title
|
||||||
|
: tabState.url.toString();
|
||||||
|
|
||||||
|
final success = await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.sendTabToDevice(
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
title: title,
|
||||||
|
url: tabState.url.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
if (success) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Sent tab to ${device.displayName}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui_helper.showErrorMessage(
|
||||||
|
context,
|
||||||
|
'Failed to send tab',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuController.maybeOf(context)?.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
},
|
||||||
|
loading: () => const [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(Icons.devices_other),
|
||||||
|
child: Text('Loading devices...'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
error: (_, _) => const [
|
||||||
|
MenuItemButton(child: Text('Failed to load devices')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Text('Send To Device'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShareMenuItemButton(selectedTabId: selectedTabId),
|
ShareMenuItemButton(selectedTabId: selectedTabId),
|
||||||
|
SendTabToDeviceMenuItemButton(selectedTabId: selectedTabId),
|
||||||
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
||||||
],
|
],
|
||||||
leadingIcon: const Icon(Icons.share),
|
leadingIcon: const Icon(Icons.share),
|
||||||
|
|||||||
+59
-2
@@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
class _TabDraggable extends HookConsumerWidget {
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
@@ -149,8 +150,58 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// final screenWidth = MediaQuery.of(context).size.width;
|
final scope = ref.watch(effectiveTabsTrayScopeProvider);
|
||||||
|
|
||||||
|
return switch (scope) {
|
||||||
|
TabsTrayScope.synced => _buildSyncedTabsView(context, ref),
|
||||||
|
_ => _buildLocalTabsView(context, ref),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSyncedTabsView(BuildContext context, WidgetRef ref) {
|
||||||
|
final syncedTabs = ref.watch(syncedTabsForSelectedDeviceProvider);
|
||||||
|
|
||||||
|
return syncedTabs.when(
|
||||||
|
skipLoadingOnReload: true,
|
||||||
|
data: (tabs) {
|
||||||
|
if (tabs.isEmpty) {
|
||||||
|
return const Center(child: Text('No synced tabs available'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: tabs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final tab = tabs[index].tab;
|
||||||
|
final uri = Uri.tryParse(tab.url);
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SyncedListTabPreview(
|
||||||
|
title: tab.title.isNotEmpty ? tab.title : tab.url,
|
||||||
|
url: uri,
|
||||||
|
deviceName: tabs[index].deviceName,
|
||||||
|
onTap: () async {
|
||||||
|
await OpenSharedContentRoute(
|
||||||
|
sharedUrl: uri.toString(),
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) =>
|
||||||
|
Center(child: Text('Failed to load synced tabs: $error')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) {
|
||||||
final containerId = ref.watch(selectedContainerProvider);
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
final filteredTabEntities = ref.watch(
|
final filteredTabEntities = ref.watch(
|
||||||
@@ -417,6 +468,12 @@ class ViewTabListWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isSyncedScope = ref.watch(
|
||||||
|
effectiveTabsTrayScopeProvider.select(
|
||||||
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final isFabVisible = useState(true);
|
final isFabVisible = useState(true);
|
||||||
final lastSheetSize = useRef(0.0);
|
final lastSheetSize = useRef(0.0);
|
||||||
final isInitialized = useRef(false);
|
final isInitialized = useRef(false);
|
||||||
@@ -516,7 +573,7 @@ class ViewTabListWidget extends HookConsumerWidget {
|
|||||||
onClose: onClose,
|
onClose: onClose,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showNewTabFab)
|
if (showNewTabFab && !isSyncedScope)
|
||||||
AnimatedSlide(
|
AnimatedSlide(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
||||||
|
|||||||
+46
@@ -36,6 +36,7 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
|
|||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class GridTabItemContainer extends StatelessWidget {
|
class GridTabItemContainer extends StatelessWidget {
|
||||||
@@ -378,6 +379,51 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SyncedListTabPreview extends StatelessWidget {
|
||||||
|
const SyncedListTabPreview({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.url,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Uri url;
|
||||||
|
final String deviceName;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 8, right: 6),
|
||||||
|
leading: UrlIcon([url], iconSize: 20),
|
||||||
|
title: Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.devices, size: 14),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
deviceName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
UriBreadcrumb(uri: url),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.open_in_new),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SingleGridTabPreview extends HookConsumerWidget {
|
class SingleGridTabPreview extends HookConsumerWidget {
|
||||||
final String tabId;
|
final String tabId;
|
||||||
final String? activeTabId;
|
final String? activeTabId;
|
||||||
|
|||||||
+279
-172
@@ -43,6 +43,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
||||||
import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart';
|
import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
@@ -50,6 +51,151 @@ import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
|||||||
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
/// Widget for tab filters (container chips with synced option)
|
||||||
|
class _TabFilters extends ConsumerWidget {
|
||||||
|
final TabsViewMode tabsViewMode;
|
||||||
|
|
||||||
|
const _TabFilters({required this.tabsViewMode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isSyncedScope = ref.watch(
|
||||||
|
effectiveTabsTrayScopeProvider.select(
|
||||||
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedContainer = ref.watch(
|
||||||
|
selectedContainerDataProvider.select((value) => value.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||||
|
final syncedTabCountAsync = ref.watch(syncedTabsTotalCountProvider);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ContainerChips(
|
||||||
|
showGroupSuggestions: switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
},
|
||||||
|
enableDragAndDrop: switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
},
|
||||||
|
showSyncedChip: isAuthenticated && tabsViewMode != TabsViewMode.tree,
|
||||||
|
syncedChipSelected: isSyncedScope,
|
||||||
|
syncedTabCount: syncedTabCountAsync.when(
|
||||||
|
data: (count) => count,
|
||||||
|
loading: () => 0,
|
||||||
|
error: (_, _) => 0,
|
||||||
|
),
|
||||||
|
onSyncedChipSelected: () {
|
||||||
|
ref.read(tabsTrayScopeControllerProvider.notifier).showSynced();
|
||||||
|
},
|
||||||
|
selectedContainer: selectedContainer,
|
||||||
|
onSelected: (container) async {
|
||||||
|
ref.read(tabsTrayScopeControllerProvider.notifier).showLocal();
|
||||||
|
|
||||||
|
if (container != null) {
|
||||||
|
final result = await ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.setContainerId(container.id);
|
||||||
|
|
||||||
|
if (context.mounted &&
|
||||||
|
result == SetContainerResult.successHasProxy) {
|
||||||
|
final shouldStartProxy = await ref
|
||||||
|
.read(startProxyControllerProvider.notifier)
|
||||||
|
.shouldPromptProxyStart();
|
||||||
|
|
||||||
|
if (!context.mounted || !shouldStartProxy) return;
|
||||||
|
|
||||||
|
final dialogResult = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return const TorDialog();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dialogResult == true) {
|
||||||
|
await ref
|
||||||
|
.read(startProxyControllerProvider.notifier)
|
||||||
|
.startProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ref.read(selectedContainerProvider.notifier).clearContainer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleted: (container) {
|
||||||
|
ref.read(tabsTrayScopeControllerProvider.notifier).showLocal();
|
||||||
|
ref.read(selectedContainerProvider.notifier).clearContainer();
|
||||||
|
},
|
||||||
|
onLongPress: (container) async {
|
||||||
|
await ContainerEditRoute(
|
||||||
|
containerData: jsonEncode(container.toJson()),
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isSyncedScope) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_SyncedDeviceSelector(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget for synced device selector
|
||||||
|
class _SyncedDeviceSelector extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final remoteDevicesAsync = ref.watch(syncRemoteTabsProvider);
|
||||||
|
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||||
|
|
||||||
|
return remoteDevicesAsync.when(
|
||||||
|
skipLoadingOnReload: true,
|
||||||
|
data: (devices) {
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveSelectedDeviceId =
|
||||||
|
selectedDeviceId != null &&
|
||||||
|
devices.any((device) => device.deviceId == selectedDeviceId)
|
||||||
|
? selectedDeviceId
|
||||||
|
: devices.first.deviceId;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 36,
|
||||||
|
child: ListView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: devices
|
||||||
|
.map((device) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6.0),
|
||||||
|
child: ChoiceChip(
|
||||||
|
label: Text(device.deviceName),
|
||||||
|
selected: effectiveSelectedDeviceId == device.deviceId,
|
||||||
|
onSelected: (_) {
|
||||||
|
ref
|
||||||
|
.read(selectedSyncedTabsDeviceIdProvider.notifier)
|
||||||
|
.selectDevice(device.deviceId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList(growable: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabViewHeader extends HookConsumerWidget {
|
class TabViewHeader extends HookConsumerWidget {
|
||||||
static const headerSize = 124.0;
|
static const headerSize = 124.0;
|
||||||
|
|
||||||
@@ -82,6 +228,13 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final selectedContainerId = ref.watch(selectedContainerProvider);
|
||||||
|
final isSyncedScope = ref.watch(
|
||||||
|
effectiveTabsTrayScopeProvider.select(
|
||||||
|
(scope) => scope == TabsTrayScope.synced,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
useOnListenableChange(searchTextController, () async {
|
useOnListenableChange(searchTextController, () async {
|
||||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||||
await ref
|
await ref
|
||||||
@@ -153,13 +306,15 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
.toList(),
|
.toList(),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
tooltip: 'Change view mode',
|
tooltip: 'Change view mode',
|
||||||
onPressed: () {
|
onPressed: isSyncedScope
|
||||||
if (viewModeMenuController.isOpen) {
|
? null
|
||||||
viewModeMenuController.close();
|
: () {
|
||||||
} else {
|
if (viewModeMenuController.isOpen) {
|
||||||
viewModeMenuController.open();
|
viewModeMenuController.close();
|
||||||
}
|
} else {
|
||||||
},
|
viewModeMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
icon: Row(
|
icon: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -278,122 +433,133 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
menuChildren: [
|
menuChildren: [
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.closeCircle),
|
leadingIcon: const Icon(MdiIcons.closeCircle),
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final result = await showCloseAllTabsDialog(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
final count = await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.closeContainerTabs(
|
||||||
|
selectedContainerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.undoClose,
|
||||||
|
count: count.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const Text('Close All Tabs'),
|
child: const Text('Close All Tabs'),
|
||||||
onPressed: () async {
|
|
||||||
final result = await showCloseAllTabsDialog(
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
final container = ref.read(
|
|
||||||
selectedContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
final count = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.closeContainerTabs(container);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.undoClose,
|
|
||||||
count: count.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.incognitoCircleOff),
|
leadingIcon: const Icon(MdiIcons.incognitoCircleOff),
|
||||||
|
onPressed: isSyncedScope
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final result =
|
||||||
|
await showCloseAllPrivateTabsDialog(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
final count = await ref
|
||||||
|
.read(
|
||||||
|
tabDataRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.closeContainerTabs(
|
||||||
|
selectedContainerId,
|
||||||
|
includeRegular: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.undoClose,
|
||||||
|
count: count.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const Text('Close Private Tabs'),
|
child: const Text('Close Private Tabs'),
|
||||||
onPressed: () async {
|
|
||||||
final result = await showCloseAllPrivateTabsDialog(
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
final container = ref.read(
|
|
||||||
selectedContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
final count = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.closeContainerTabs(
|
|
||||||
container,
|
|
||||||
includeRegular: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.undoClose,
|
|
||||||
count: count.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||||
child: const Text('Bookmark all'),
|
onPressed: isSyncedScope
|
||||||
onPressed: () async {
|
? null
|
||||||
final choice = await showBookmarkAllDialog(context);
|
: () async {
|
||||||
if (choice == null || !context.mounted) return;
|
final choice = await showBookmarkAllDialog(
|
||||||
|
context,
|
||||||
final containerId = ref.read(
|
|
||||||
selectedContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
final tabData = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.getContainerTabsData(containerId);
|
|
||||||
|
|
||||||
if (choice == BookmarkAllChoice.fast) {
|
|
||||||
if (!context.mounted) return;
|
|
||||||
final folderGuid = await showSelectFolderDialog(
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
if (folderGuid == null) return;
|
|
||||||
|
|
||||||
final repo = ref.read(
|
|
||||||
bookmarksRepositoryProvider.notifier,
|
|
||||||
);
|
|
||||||
for (final tab in tabData) {
|
|
||||||
if (tab.url != null) {
|
|
||||||
await repo.addBookmark(
|
|
||||||
parentGuid: folderGuid,
|
|
||||||
url: tab.url!,
|
|
||||||
title: tab.title ?? tab.url.toString(),
|
|
||||||
);
|
);
|
||||||
}
|
if (choice == null || !context.mounted) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
final tabData = await ref
|
||||||
ui_helper.showInfoMessage(
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
context,
|
.getContainerTabsData(
|
||||||
'${tabData.length} bookmark(s) added',
|
selectedContainerId,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
if (choice == BookmarkAllChoice.fast) {
|
||||||
for (final tab in tabData) {
|
if (!context.mounted) return;
|
||||||
if (context.mounted) {
|
final folderGuid =
|
||||||
await BookmarkEntryAddRoute(
|
await showSelectFolderDialog(context);
|
||||||
bookmarkInfo: jsonEncode(
|
if (folderGuid == null) return;
|
||||||
BookmarkInfo(
|
|
||||||
title: tab.title,
|
final repo = ref.read(
|
||||||
url: tab.url.toString(),
|
bookmarksRepositoryProvider.notifier,
|
||||||
).encode(),
|
);
|
||||||
),
|
for (final tab in tabData) {
|
||||||
).push(context);
|
if (tab.url != null) {
|
||||||
}
|
await repo.addBookmark(
|
||||||
}
|
parentGuid: folderGuid,
|
||||||
}
|
url: tab.url!,
|
||||||
},
|
title:
|
||||||
|
tab.title ?? tab.url.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'${tabData.length} bookmark(s) added',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (final tab in tabData) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await BookmarkEntryAddRoute(
|
||||||
|
bookmarkInfo: jsonEncode(
|
||||||
|
BookmarkInfo(
|
||||||
|
title: tab.title,
|
||||||
|
url: tab.url.toString(),
|
||||||
|
).encode(),
|
||||||
|
),
|
||||||
|
).push(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Bookmark all'),
|
||||||
),
|
),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
@@ -582,66 +748,7 @@ class TabViewHeader extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final selectedContainer = ref.watch(
|
return _TabFilters(tabsViewMode: tabsViewMode);
|
||||||
selectedContainerDataProvider.select(
|
|
||||||
(value) => value.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ContainerChips(
|
|
||||||
showGroupSuggestions: switch (tabsViewMode) {
|
|
||||||
TabsViewMode.list || TabsViewMode.grid => true,
|
|
||||||
TabsViewMode.tree => false,
|
|
||||||
},
|
|
||||||
enableDragAndDrop: switch (tabsViewMode) {
|
|
||||||
TabsViewMode.list || TabsViewMode.grid => true,
|
|
||||||
TabsViewMode.tree => false,
|
|
||||||
},
|
|
||||||
selectedContainer: selectedContainer,
|
|
||||||
onSelected: (container) async {
|
|
||||||
if (container != null) {
|
|
||||||
final result = await ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.setContainerId(container.id);
|
|
||||||
|
|
||||||
if (context.mounted &&
|
|
||||||
result == SetContainerResult.successHasProxy) {
|
|
||||||
final shouldStartProxy = await ref
|
|
||||||
.read(startProxyControllerProvider.notifier)
|
|
||||||
.shouldPromptProxyStart();
|
|
||||||
|
|
||||||
if (!context.mounted || !shouldStartProxy) return;
|
|
||||||
|
|
||||||
final dialogResult = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return const TorDialog();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dialogResult == true) {
|
|
||||||
await ref
|
|
||||||
.read(startProxyControllerProvider.notifier)
|
|
||||||
.startProxy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.clearContainer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDeleted: (container) {
|
|
||||||
ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.clearContainer();
|
|
||||||
},
|
|
||||||
onLongPress: (container) async {
|
|
||||||
await ContainerEditRoute(
|
|
||||||
containerData: jsonEncode(container.toJson()),
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
+1
-1
@@ -178,7 +178,7 @@ class FeedSearch extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
ref
|
ref
|
||||||
.read(formatProvider.notifier)
|
.read(formatProvider.notifier)
|
||||||
.fullDateTimeWithTimezone(articleDate),
|
.fullDateTime(articleDate),
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleSelectable<String> siteAssignedContainerId(Uri uri) {
|
Selectable<String> siteAssignedContainerId(Uri uri) {
|
||||||
return db.definitionsDrift.siteAssignedContainerId(uri: uri.origin);
|
return db.definitionsDrift.siteAssignedContainerId(uri: uri.origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
.containerDao
|
.containerDao
|
||||||
.siteAssignedContainerId(uri)
|
.siteAssignedContainerId(uri)
|
||||||
.getSingle();
|
.get()
|
||||||
|
.then((value) => value.firstOrNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getContainersToClearOnExit() async {
|
Future<List<String>> getContainersToClearOnExit() async {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerRepositoryHash() =>
|
String _$containerRepositoryHash() =>
|
||||||
r'62c2b06970db4385ea0d2edc68bcf076c291a9b6';
|
r'5e46b6d9d3510aeeb70646ede75d71c2db9efb0f';
|
||||||
|
|
||||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
|||||||
|
|
||||||
class _UnassignedContainerChip extends ConsumerWidget {
|
class _UnassignedContainerChip extends ConsumerWidget {
|
||||||
final int? Function()? containerBadgeCount;
|
final int? Function()? containerBadgeCount;
|
||||||
final ContainerData? selectedContainer;
|
final bool selected;
|
||||||
final void Function(ContainerDataWithCount?)? onSelected;
|
final void Function(ContainerDataWithCount?)? onSelected;
|
||||||
|
|
||||||
const _UnassignedContainerChip({
|
const _UnassignedContainerChip({
|
||||||
required this.containerBadgeCount,
|
required this.containerBadgeCount,
|
||||||
required this.selectedContainer,
|
required this.selected,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class _UnassignedContainerChip extends ConsumerWidget {
|
|||||||
label: (tabCount > 0)
|
label: (tabCount > 0)
|
||||||
? Text(tabCount.toString())
|
? Text(tabCount.toString())
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
selected: selectedContainer == null,
|
selected: selected,
|
||||||
showCheckmark: false,
|
showCheckmark: false,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -76,6 +76,34 @@ class _UnassignedContainerChip extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SyncedTabsChip extends ConsumerWidget {
|
||||||
|
final bool selected;
|
||||||
|
final int count;
|
||||||
|
final VoidCallback onSelected;
|
||||||
|
|
||||||
|
const _SyncedTabsChip({
|
||||||
|
required this.selected,
|
||||||
|
required this.count,
|
||||||
|
required this.onSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return FilterChip(
|
||||||
|
avatar: const Icon(Icons.devices_other),
|
||||||
|
labelPadding: count > 0 ? null : const EdgeInsets.only(right: 2.0),
|
||||||
|
label: count > 0 ? Text(count.toString()) : const SizedBox.shrink(),
|
||||||
|
selected: selected,
|
||||||
|
showCheckmark: false,
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value) {
|
||||||
|
onSelected();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ContainerSuggestionsChip extends ConsumerWidget {
|
class _ContainerSuggestionsChip extends ConsumerWidget {
|
||||||
const _ContainerSuggestionsChip();
|
const _ContainerSuggestionsChip();
|
||||||
|
|
||||||
@@ -135,6 +163,11 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
final bool showUnassignedChip;
|
final bool showUnassignedChip;
|
||||||
final bool showGroupSuggestions;
|
final bool showGroupSuggestions;
|
||||||
final bool enableDragAndDrop;
|
final bool enableDragAndDrop;
|
||||||
|
final bool showSyncedChip;
|
||||||
|
final bool syncedChipSelected;
|
||||||
|
final bool? unassignedChipSelected;
|
||||||
|
final int syncedTabCount;
|
||||||
|
final VoidCallback? onSyncedChipSelected;
|
||||||
|
|
||||||
final ContainerData? selectedContainer;
|
final ContainerData? selectedContainer;
|
||||||
final bool Function(ContainerDataWithCount)? containerFilter;
|
final bool Function(ContainerDataWithCount)? containerFilter;
|
||||||
@@ -157,6 +190,11 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
this.showUnassignedChip = true,
|
this.showUnassignedChip = true,
|
||||||
this.showGroupSuggestions = false,
|
this.showGroupSuggestions = false,
|
||||||
this.enableDragAndDrop = true,
|
this.enableDragAndDrop = true,
|
||||||
|
this.showSyncedChip = false,
|
||||||
|
this.syncedChipSelected = false,
|
||||||
|
this.unassignedChipSelected,
|
||||||
|
this.syncedTabCount = 0,
|
||||||
|
this.onSyncedChipSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -217,6 +255,12 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
prefixListItems: [
|
prefixListItems: [
|
||||||
|
if (showSyncedChip)
|
||||||
|
_SyncedTabsChip(
|
||||||
|
selected: syncedChipSelected,
|
||||||
|
count: syncedTabCount,
|
||||||
|
onSelected: onSyncedChipSelected ?? () {},
|
||||||
|
),
|
||||||
if (showUnassignedChip)
|
if (showUnassignedChip)
|
||||||
enableDragAndDrop
|
enableDragAndDrop
|
||||||
? TabDragContainerTarget(
|
? TabDragContainerTarget(
|
||||||
@@ -224,14 +268,20 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
child: _UnassignedContainerChip(
|
child: _UnassignedContainerChip(
|
||||||
containerBadgeCount: () =>
|
containerBadgeCount: () =>
|
||||||
containerBadgeCount?.call(null),
|
containerBadgeCount?.call(null),
|
||||||
selectedContainer: selectedContainer,
|
selected:
|
||||||
|
unassignedChipSelected ??
|
||||||
|
(selectedContainer == null &&
|
||||||
|
!syncedChipSelected),
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _UnassignedContainerChip(
|
: _UnassignedContainerChip(
|
||||||
containerBadgeCount: () =>
|
containerBadgeCount: () =>
|
||||||
containerBadgeCount?.call(null),
|
containerBadgeCount?.call(null),
|
||||||
selectedContainer: selectedContainer,
|
selected:
|
||||||
|
unassignedChipSelected ??
|
||||||
|
(selectedContainer == null &&
|
||||||
|
!syncedChipSelected),
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
),
|
),
|
||||||
if (showGroupSuggestions)
|
if (showGroupSuggestions)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
_PrivacySecurityTile(),
|
_PrivacySecurityTile(),
|
||||||
_SearchContentTile(),
|
_SearchContentTile(),
|
||||||
_TabsBehaviorTile(),
|
_TabsBehaviorTile(),
|
||||||
|
_SyncTile(),
|
||||||
_FingerprintingTile(),
|
_FingerprintingTile(),
|
||||||
_AdvancedTile(),
|
_AdvancedTile(),
|
||||||
],
|
],
|
||||||
@@ -176,6 +177,31 @@ class _FingerprintingTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SyncTile extends StatelessWidget {
|
||||||
|
const _SyncTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Theme.of(context).highlightColor,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: ListTile(
|
||||||
|
title: const Text('Firefox Sync'),
|
||||||
|
subtitle: const Text('Account, sync now, engine selection'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.sync),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await SyncSettingsRoute().push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AdvancedTile extends StatelessWidget {
|
class _AdvancedTile extends StatelessWidget {
|
||||||
const _AdvancedTile();
|
const _AdvancedTile();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
|
||||||
|
part 'sync_repository_state.g.dart';
|
||||||
|
|
||||||
|
enum SyncEvent {
|
||||||
|
started,
|
||||||
|
completed,
|
||||||
|
error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
|
class SyncRepositoryState with FastEquatable {
|
||||||
|
final SyncAccountInfo account;
|
||||||
|
final List<SyncDeviceTabs> remoteTabs;
|
||||||
|
final List<SyncDevice> devices;
|
||||||
|
final String? deviceName;
|
||||||
|
final SyncEvent? lastSyncEvent;
|
||||||
|
final String? lastSyncError;
|
||||||
|
|
||||||
|
SyncRepositoryState({
|
||||||
|
required this.account,
|
||||||
|
this.remoteTabs = const <SyncDeviceTabs>[],
|
||||||
|
this.devices = const <SyncDevice>[],
|
||||||
|
this.deviceName,
|
||||||
|
this.lastSyncEvent,
|
||||||
|
this.lastSyncError,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters =>
|
||||||
|
[account, remoteTabs, devices, deviceName, lastSyncEvent, lastSyncError];
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sync_repository_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$SyncRepositoryStateCWProxy {
|
||||||
|
SyncRepositoryState account(SyncAccountInfo account);
|
||||||
|
|
||||||
|
SyncRepositoryState remoteTabs(List<SyncDeviceTabs> remoteTabs);
|
||||||
|
|
||||||
|
SyncRepositoryState devices(List<SyncDevice> devices);
|
||||||
|
|
||||||
|
SyncRepositoryState deviceName(String? deviceName);
|
||||||
|
|
||||||
|
SyncRepositoryState lastSyncEvent(SyncEvent? lastSyncEvent);
|
||||||
|
|
||||||
|
SyncRepositoryState lastSyncError(String? lastSyncError);
|
||||||
|
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SyncRepositoryState(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// SyncRepositoryState(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
SyncRepositoryState call({
|
||||||
|
SyncAccountInfo account,
|
||||||
|
List<SyncDeviceTabs> remoteTabs,
|
||||||
|
List<SyncDevice> devices,
|
||||||
|
String? deviceName,
|
||||||
|
SyncEvent? lastSyncEvent,
|
||||||
|
String? lastSyncError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callable proxy for `copyWith` functionality.
|
||||||
|
/// Use as `instanceOfSyncRepositoryState.copyWith(...)` or call `instanceOfSyncRepositoryState.copyWith.fieldName(value)` for a single field.
|
||||||
|
class _$SyncRepositoryStateCWProxyImpl implements _$SyncRepositoryStateCWProxy {
|
||||||
|
const _$SyncRepositoryStateCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final SyncRepositoryState _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState account(SyncAccountInfo account) =>
|
||||||
|
call(account: account);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState remoteTabs(List<SyncDeviceTabs> remoteTabs) =>
|
||||||
|
call(remoteTabs: remoteTabs);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState devices(List<SyncDevice> devices) =>
|
||||||
|
call(devices: devices);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState deviceName(String? deviceName) =>
|
||||||
|
call(deviceName: deviceName);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState lastSyncEvent(SyncEvent? lastSyncEvent) =>
|
||||||
|
call(lastSyncEvent: lastSyncEvent);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SyncRepositoryState lastSyncError(String? lastSyncError) =>
|
||||||
|
call(lastSyncError: lastSyncError);
|
||||||
|
|
||||||
|
@override
|
||||||
|
/// Creates a new instance with the provided field values.
|
||||||
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SyncRepositoryState(...).copyWith.fieldName(value)`.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// SyncRepositoryState(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ```
|
||||||
|
SyncRepositoryState call({
|
||||||
|
Object? account = const $CopyWithPlaceholder(),
|
||||||
|
Object? remoteTabs = const $CopyWithPlaceholder(),
|
||||||
|
Object? devices = const $CopyWithPlaceholder(),
|
||||||
|
Object? deviceName = const $CopyWithPlaceholder(),
|
||||||
|
Object? lastSyncEvent = const $CopyWithPlaceholder(),
|
||||||
|
Object? lastSyncError = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return SyncRepositoryState(
|
||||||
|
account: account == const $CopyWithPlaceholder() || account == null
|
||||||
|
? _value.account
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: account as SyncAccountInfo,
|
||||||
|
remoteTabs:
|
||||||
|
remoteTabs == const $CopyWithPlaceholder() || remoteTabs == null
|
||||||
|
? _value.remoteTabs
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: remoteTabs as List<SyncDeviceTabs>,
|
||||||
|
devices: devices == const $CopyWithPlaceholder() || devices == null
|
||||||
|
? _value.devices
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: devices as List<SyncDevice>,
|
||||||
|
deviceName: deviceName == const $CopyWithPlaceholder()
|
||||||
|
? _value.deviceName
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: deviceName as String?,
|
||||||
|
lastSyncEvent: lastSyncEvent == const $CopyWithPlaceholder()
|
||||||
|
? _value.lastSyncEvent
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: lastSyncEvent as SyncEvent?,
|
||||||
|
lastSyncError: lastSyncError == const $CopyWithPlaceholder()
|
||||||
|
? _value.lastSyncError
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: lastSyncError as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $SyncRepositoryStateCopyWith on SyncRepositoryState {
|
||||||
|
/// Returns a callable class used to build a new instance with modified fields.
|
||||||
|
/// Example: `instanceOfSyncRepositoryState.copyWith(...)` or `instanceOfSyncRepositoryState.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$SyncRepositoryStateCWProxy get copyWith =>
|
||||||
|
_$SyncRepositoryStateCWProxyImpl(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
|
||||||
|
class SyncedTabItem with FastEquatable {
|
||||||
|
final String deviceId;
|
||||||
|
final String deviceName;
|
||||||
|
final SyncRemoteTab tab;
|
||||||
|
|
||||||
|
SyncedTabItem({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.tab,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [deviceId, deviceName, tab];
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/core/logger.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/entities/synced_tab_item.dart';
|
||||||
|
|
||||||
|
part 'sync.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
bool syncIsAuthenticated(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRepositoryProvider.select(
|
||||||
|
(value) => value.value?.account.authenticated == true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TabsTrayScope { local, synced }
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class TabsTrayScopeController extends _$TabsTrayScopeController {
|
||||||
|
@override
|
||||||
|
TabsTrayScope build() => TabsTrayScope.local;
|
||||||
|
|
||||||
|
void showLocal() {
|
||||||
|
state = TabsTrayScope.local;
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSynced() {
|
||||||
|
state = TabsTrayScope.synced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
TabsTrayScope effectiveTabsTrayScope(Ref ref) {
|
||||||
|
final scope = ref.watch(tabsTrayScopeControllerProvider);
|
||||||
|
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||||
|
|
||||||
|
if (!isAuthenticated && scope == TabsTrayScope.synced) {
|
||||||
|
return TabsTrayScope.local;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class SelectedSyncedTabsDeviceId extends _$SelectedSyncedTabsDeviceId {
|
||||||
|
@override
|
||||||
|
String? build() => null;
|
||||||
|
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void selectDevice(String? value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<int> syncedTabsTotalCount(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRemoteTabsProvider.selectAsync(
|
||||||
|
(devices) =>
|
||||||
|
devices.fold<int>(0, (count, device) => count + device.tabs.length),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<String?> effectiveSyncedTabsDeviceId(Ref ref) async {
|
||||||
|
final devices = await ref.watch(
|
||||||
|
syncRemoteTabsProvider.selectAsync((tabs) => tabs),
|
||||||
|
);
|
||||||
|
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||||
|
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedDeviceId != null &&
|
||||||
|
devices.any((device) => device.deviceId == selectedDeviceId)) {
|
||||||
|
return selectedDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices.first.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SyncedTabItem>> syncedTabsForSelectedDevice(Ref ref) async {
|
||||||
|
final devices = await ref.watch(
|
||||||
|
syncRemoteTabsProvider.selectAsync((tabs) => tabs),
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||||
|
|
||||||
|
final effectiveSelectedDeviceId =
|
||||||
|
selectedDeviceId != null &&
|
||||||
|
devices.any((device) => device.deviceId == selectedDeviceId)
|
||||||
|
? selectedDeviceId
|
||||||
|
: devices.firstOrNull?.deviceId;
|
||||||
|
|
||||||
|
if (effectiveSelectedDeviceId == null) {
|
||||||
|
return const <SyncedTabItem>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final device = devices.firstWhereOrNull(
|
||||||
|
(item) => item.deviceId == effectiveSelectedDeviceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
return const <SyncedTabItem>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final tabs = device.tabs
|
||||||
|
.map(
|
||||||
|
(tab) => SyncedTabItem(
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
deviceName: device.deviceName,
|
||||||
|
tab: tab,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
tabs.sort((a, b) => b.tab.lastUsed.compareTo(a.tab.lastUsed));
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class SyncRepository extends _$SyncRepository {
|
||||||
|
final _service = GeckoSyncService();
|
||||||
|
|
||||||
|
StreamSubscription<SyncAccountInfo>? _authStateSub;
|
||||||
|
StreamSubscription<void>? _syncStartedSub;
|
||||||
|
StreamSubscription<void>? _syncCompletedSub;
|
||||||
|
StreamSubscription<String?>? _syncErrorSub;
|
||||||
|
|
||||||
|
Future<void> _awaitInitialized() => future;
|
||||||
|
|
||||||
|
void _update(SyncRepositoryState Function(SyncRepositoryState) updater) {
|
||||||
|
final current = state.value;
|
||||||
|
if (current != null) {
|
||||||
|
state = AsyncData(updater(current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshAccount() async {
|
||||||
|
await _awaitInitialized();
|
||||||
|
|
||||||
|
final account = await _service.getAccountInfo();
|
||||||
|
_update((s) => s.copyWith(account: account));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshTabs() async {
|
||||||
|
await _awaitInitialized();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final tabs = await _service.getSyncedTabs();
|
||||||
|
_update((s) => s.copyWith(remoteTabs: tabs));
|
||||||
|
} on Exception catch (e) {
|
||||||
|
logger.e('Failed to refresh synced tabs', error: e);
|
||||||
|
_update(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
lastSyncEvent: SyncEvent.error,
|
||||||
|
lastSyncError: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshDevices() async {
|
||||||
|
await _awaitInitialized();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final devices = await _service.getDevices();
|
||||||
|
_update((s) => s.copyWith(devices: devices));
|
||||||
|
} on Exception catch (e) {
|
||||||
|
logger.e('Failed to refresh devices', error: e);
|
||||||
|
_update(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
lastSyncEvent: SyncEvent.error,
|
||||||
|
lastSyncError: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshDeviceName() async {
|
||||||
|
await _awaitInitialized();
|
||||||
|
|
||||||
|
final deviceName = await _service.getDeviceName();
|
||||||
|
_update((s) => s.copyWith(deviceName: deviceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SyncAccountInfo> refresh() async {
|
||||||
|
await _refreshAccount();
|
||||||
|
return state.value!.account;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signIn() async {
|
||||||
|
await _service.beginAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signInWithPairing(String pairingUrl) async {
|
||||||
|
await _service.beginPairingAuthentication(pairingUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
await _service.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> syncNow() async {
|
||||||
|
await _service.syncNow();
|
||||||
|
await Future.wait([_refreshAccount(), _refreshTabs(), _refreshDevices()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEngineEnabled(SyncEngineValue engine, bool enabled) async {
|
||||||
|
await _service.setEngineEnabled(engine, enabled);
|
||||||
|
await Future.wait([_refreshAccount(), _refreshTabs(), _refreshDevices()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> sendTabToDevice({
|
||||||
|
required String deviceId,
|
||||||
|
required String title,
|
||||||
|
required String url,
|
||||||
|
}) {
|
||||||
|
return _service.sendTabToDevice(deviceId, title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setDeviceName(String newName) async {
|
||||||
|
final result = await _service.setDeviceName(newName);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await Future.wait([_refreshDevices(), _refreshDeviceName()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshDevices() async {
|
||||||
|
await _service.refreshDevices();
|
||||||
|
await _refreshDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> pollIncomingTabsAndOpen() async {
|
||||||
|
await _service.pollDeviceCommands();
|
||||||
|
final incomingTabs = await _service.drainIncomingTabs();
|
||||||
|
|
||||||
|
for (final tab in incomingTabs) {
|
||||||
|
await _openUrlInAssignedContainer(tab.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _refreshTabs();
|
||||||
|
return incomingTabs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openSyncedTab(SyncRemoteTab tab) async {
|
||||||
|
await _openUrlInAssignedContainer(tab.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openUrlInAssignedContainer(String url) async {
|
||||||
|
final uri = Uri.tryParse(url);
|
||||||
|
if (uri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final containerRepository = ref.read(containerRepositoryProvider.notifier);
|
||||||
|
|
||||||
|
final containerId = await containerRepository.siteAssignedContainerId(uri);
|
||||||
|
|
||||||
|
final assignedContainer = containerId == null
|
||||||
|
? null
|
||||||
|
: await containerRepository.getContainerData(containerId);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.addTab(
|
||||||
|
url: uri,
|
||||||
|
selectTab: true,
|
||||||
|
private: false,
|
||||||
|
container: Value(assignedContainer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SyncRepositoryState> build() async {
|
||||||
|
final syncStateService = ref.read(geckoSyncStateServiceProvider);
|
||||||
|
|
||||||
|
_syncStartedSub = syncStateService.syncStartedEvents.listen((_) {
|
||||||
|
_update(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
lastSyncEvent: SyncEvent.started,
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
lastSyncError: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
_syncCompletedSub = syncStateService.syncCompletedEvents.listen((_) {
|
||||||
|
_update(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
lastSyncEvent: SyncEvent.completed,
|
||||||
|
// ignore: avoid_redundant_argument_values
|
||||||
|
lastSyncError: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
unawaited(Future.wait([_refreshTabs(), _refreshDevices()]));
|
||||||
|
});
|
||||||
|
|
||||||
|
_syncErrorSub = syncStateService.syncErrorEvents.listen((error) {
|
||||||
|
logger.e('Sync failed', error: error ?? 'Unknown synchronization error');
|
||||||
|
_update(
|
||||||
|
(s) => s.copyWith(lastSyncEvent: SyncEvent.error, lastSyncError: error),
|
||||||
|
);
|
||||||
|
unawaited(Future.wait([_refreshTabs(), _refreshDevices()]));
|
||||||
|
});
|
||||||
|
|
||||||
|
_authStateSub = syncStateService.authStateEvents.listen((info) {
|
||||||
|
final previous = state.value;
|
||||||
|
|
||||||
|
if (previous == null) {
|
||||||
|
state = AsyncData(SyncRepositoryState(account: info));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final unauthenticated = !info.authenticated;
|
||||||
|
state = AsyncData(
|
||||||
|
unauthenticated
|
||||||
|
? SyncRepositoryState(account: info)
|
||||||
|
: previous.copyWith(account: info),
|
||||||
|
);
|
||||||
|
|
||||||
|
final authStateChanged =
|
||||||
|
previous.account.authenticated != info.authenticated ||
|
||||||
|
previous.account.needsReauth != info.needsReauth;
|
||||||
|
|
||||||
|
if (authStateChanged && info.authenticated) {
|
||||||
|
unawaited(
|
||||||
|
Future.wait([
|
||||||
|
_refreshAccount(),
|
||||||
|
_refreshTabs(),
|
||||||
|
_refreshDevices(),
|
||||||
|
_refreshDeviceName(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
unawaited(_authStateSub?.cancel());
|
||||||
|
unawaited(_syncStartedSub?.cancel());
|
||||||
|
unawaited(_syncCompletedSub?.cancel());
|
||||||
|
unawaited(_syncErrorSub?.cancel());
|
||||||
|
});
|
||||||
|
|
||||||
|
final (account, tabs, devices, deviceName) = await (
|
||||||
|
_service.getAccountInfo(),
|
||||||
|
_service.getSyncedTabs(),
|
||||||
|
_service.getDevices(),
|
||||||
|
_service.getDeviceName(),
|
||||||
|
).wait;
|
||||||
|
|
||||||
|
return SyncRepositoryState(
|
||||||
|
account: account,
|
||||||
|
remoteTabs: tabs,
|
||||||
|
devices: devices,
|
||||||
|
deviceName: deviceName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
GeckoSyncStateService geckoSyncStateService(Ref ref) {
|
||||||
|
final service = GeckoSyncStateService.setUp();
|
||||||
|
ref.onDispose(() => service.dispose());
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<String?> syncDeviceName(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRepositoryProvider.selectAsync((value) => value.deviceName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SyncDeviceTabs>> syncRemoteTabs(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRepositoryProvider.selectAsync((value) => value.remoteTabs),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SyncDevice>> syncDevices(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRepositoryProvider.selectAsync((value) => value.devices),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<(SyncEvent?, String?)> syncEvent(Ref ref) {
|
||||||
|
return ref.watch(
|
||||||
|
syncRepositoryProvider.selectAsync(
|
||||||
|
(s) => (s.lastSyncEvent, s.lastSyncError),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,560 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sync.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(syncIsAuthenticated)
|
||||||
|
final syncIsAuthenticatedProvider = SyncIsAuthenticatedProvider._();
|
||||||
|
|
||||||
|
final class SyncIsAuthenticatedProvider
|
||||||
|
extends $FunctionalProvider<bool, bool, bool>
|
||||||
|
with $Provider<bool> {
|
||||||
|
SyncIsAuthenticatedProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncIsAuthenticatedProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncIsAuthenticatedHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool create(Ref ref) {
|
||||||
|
return syncIsAuthenticated(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncIsAuthenticatedHash() =>
|
||||||
|
r'40e108a5c1d4d885fd31edde04146bf4131dd51c';
|
||||||
|
|
||||||
|
@ProviderFor(TabsTrayScopeController)
|
||||||
|
final tabsTrayScopeControllerProvider = TabsTrayScopeControllerProvider._();
|
||||||
|
|
||||||
|
final class TabsTrayScopeControllerProvider
|
||||||
|
extends $NotifierProvider<TabsTrayScopeController, TabsTrayScope> {
|
||||||
|
TabsTrayScopeControllerProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'tabsTrayScopeControllerProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$tabsTrayScopeControllerHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
TabsTrayScopeController create() => TabsTrayScopeController();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(TabsTrayScope value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<TabsTrayScope>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$tabsTrayScopeControllerHash() =>
|
||||||
|
r'e9415c1f57e1a78804ec4eee122c9db9ee416066';
|
||||||
|
|
||||||
|
abstract class _$TabsTrayScopeController extends $Notifier<TabsTrayScope> {
|
||||||
|
TabsTrayScope build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<TabsTrayScope, TabsTrayScope>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<TabsTrayScope, TabsTrayScope>,
|
||||||
|
TabsTrayScope,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(effectiveTabsTrayScope)
|
||||||
|
final effectiveTabsTrayScopeProvider = EffectiveTabsTrayScopeProvider._();
|
||||||
|
|
||||||
|
final class EffectiveTabsTrayScopeProvider
|
||||||
|
extends $FunctionalProvider<TabsTrayScope, TabsTrayScope, TabsTrayScope>
|
||||||
|
with $Provider<TabsTrayScope> {
|
||||||
|
EffectiveTabsTrayScopeProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'effectiveTabsTrayScopeProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$effectiveTabsTrayScopeHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<TabsTrayScope> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabsTrayScope create(Ref ref) {
|
||||||
|
return effectiveTabsTrayScope(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(TabsTrayScope value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<TabsTrayScope>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$effectiveTabsTrayScopeHash() =>
|
||||||
|
r'ac295d7413a7014882835d3533bf31cd3c031a1d';
|
||||||
|
|
||||||
|
@ProviderFor(SelectedSyncedTabsDeviceId)
|
||||||
|
final selectedSyncedTabsDeviceIdProvider =
|
||||||
|
SelectedSyncedTabsDeviceIdProvider._();
|
||||||
|
|
||||||
|
final class SelectedSyncedTabsDeviceIdProvider
|
||||||
|
extends $NotifierProvider<SelectedSyncedTabsDeviceId, String?> {
|
||||||
|
SelectedSyncedTabsDeviceIdProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'selectedSyncedTabsDeviceIdProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$selectedSyncedTabsDeviceIdHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SelectedSyncedTabsDeviceId create() => SelectedSyncedTabsDeviceId();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(String? value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<String?>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$selectedSyncedTabsDeviceIdHash() =>
|
||||||
|
r'61f86d2f17d6c2f04e1fb76dae900c4695aa6af0';
|
||||||
|
|
||||||
|
abstract class _$SelectedSyncedTabsDeviceId extends $Notifier<String?> {
|
||||||
|
String? build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<String?, String?>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<String?, String?>,
|
||||||
|
String?,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(syncedTabsTotalCount)
|
||||||
|
final syncedTabsTotalCountProvider = SyncedTabsTotalCountProvider._();
|
||||||
|
|
||||||
|
final class SyncedTabsTotalCountProvider
|
||||||
|
extends $FunctionalProvider<AsyncValue<int>, int, FutureOr<int>>
|
||||||
|
with $FutureModifier<int>, $FutureProvider<int> {
|
||||||
|
SyncedTabsTotalCountProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncedTabsTotalCountProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncedTabsTotalCountHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||||
|
$FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<int> create(Ref ref) {
|
||||||
|
return syncedTabsTotalCount(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncedTabsTotalCountHash() =>
|
||||||
|
r'507b14aac187c434cf02925d895151c1a25159ea';
|
||||||
|
|
||||||
|
@ProviderFor(effectiveSyncedTabsDeviceId)
|
||||||
|
final effectiveSyncedTabsDeviceIdProvider =
|
||||||
|
EffectiveSyncedTabsDeviceIdProvider._();
|
||||||
|
|
||||||
|
final class EffectiveSyncedTabsDeviceIdProvider
|
||||||
|
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
|
||||||
|
with $FutureModifier<String?>, $FutureProvider<String?> {
|
||||||
|
EffectiveSyncedTabsDeviceIdProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'effectiveSyncedTabsDeviceIdProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$effectiveSyncedTabsDeviceIdHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||||
|
$FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String?> create(Ref ref) {
|
||||||
|
return effectiveSyncedTabsDeviceId(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$effectiveSyncedTabsDeviceIdHash() =>
|
||||||
|
r'26ed7e56191a8019cab5c89dc8d5622e1a709e20';
|
||||||
|
|
||||||
|
@ProviderFor(syncedTabsForSelectedDevice)
|
||||||
|
final syncedTabsForSelectedDeviceProvider =
|
||||||
|
SyncedTabsForSelectedDeviceProvider._();
|
||||||
|
|
||||||
|
final class SyncedTabsForSelectedDeviceProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<SyncedTabItem>>,
|
||||||
|
List<SyncedTabItem>,
|
||||||
|
FutureOr<List<SyncedTabItem>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<List<SyncedTabItem>>,
|
||||||
|
$FutureProvider<List<SyncedTabItem>> {
|
||||||
|
SyncedTabsForSelectedDeviceProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncedTabsForSelectedDeviceProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncedTabsForSelectedDeviceHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<List<SyncedTabItem>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<List<SyncedTabItem>> create(Ref ref) {
|
||||||
|
return syncedTabsForSelectedDevice(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncedTabsForSelectedDeviceHash() =>
|
||||||
|
r'a85c4e972ab55b872f1de64cd27ba18d7dcc023a';
|
||||||
|
|
||||||
|
@ProviderFor(SyncRepository)
|
||||||
|
final syncRepositoryProvider = SyncRepositoryProvider._();
|
||||||
|
|
||||||
|
final class SyncRepositoryProvider
|
||||||
|
extends $AsyncNotifierProvider<SyncRepository, SyncRepositoryState> {
|
||||||
|
SyncRepositoryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncRepositoryProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncRepositoryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SyncRepository create() => SyncRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncRepositoryHash() => r'5312aed1abf60ce3e04afa7d2f60b38367c09312';
|
||||||
|
|
||||||
|
abstract class _$SyncRepository extends $AsyncNotifier<SyncRepositoryState> {
|
||||||
|
FutureOr<SyncRepositoryState> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref as $Ref<AsyncValue<SyncRepositoryState>, SyncRepositoryState>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<SyncRepositoryState>, SyncRepositoryState>,
|
||||||
|
AsyncValue<SyncRepositoryState>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(geckoSyncStateService)
|
||||||
|
final geckoSyncStateServiceProvider = GeckoSyncStateServiceProvider._();
|
||||||
|
|
||||||
|
final class GeckoSyncStateServiceProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
GeckoSyncStateService,
|
||||||
|
GeckoSyncStateService,
|
||||||
|
GeckoSyncStateService
|
||||||
|
>
|
||||||
|
with $Provider<GeckoSyncStateService> {
|
||||||
|
GeckoSyncStateServiceProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'geckoSyncStateServiceProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$geckoSyncStateServiceHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<GeckoSyncStateService> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeckoSyncStateService create(Ref ref) {
|
||||||
|
return geckoSyncStateService(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(GeckoSyncStateService value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<GeckoSyncStateService>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$geckoSyncStateServiceHash() =>
|
||||||
|
r'1e96db4a6229a3d2a31862cc9cf88c463405819b';
|
||||||
|
|
||||||
|
@ProviderFor(syncDeviceName)
|
||||||
|
final syncDeviceNameProvider = SyncDeviceNameProvider._();
|
||||||
|
|
||||||
|
final class SyncDeviceNameProvider
|
||||||
|
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
|
||||||
|
with $FutureModifier<String?>, $FutureProvider<String?> {
|
||||||
|
SyncDeviceNameProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncDeviceNameProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncDeviceNameHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||||
|
$FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String?> create(Ref ref) {
|
||||||
|
return syncDeviceName(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncDeviceNameHash() => r'3d7e531cba481c4233a895b1d80e915d646fc6b6';
|
||||||
|
|
||||||
|
@ProviderFor(syncRemoteTabs)
|
||||||
|
final syncRemoteTabsProvider = SyncRemoteTabsProvider._();
|
||||||
|
|
||||||
|
final class SyncRemoteTabsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<SyncDeviceTabs>>,
|
||||||
|
List<SyncDeviceTabs>,
|
||||||
|
FutureOr<List<SyncDeviceTabs>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<List<SyncDeviceTabs>>,
|
||||||
|
$FutureProvider<List<SyncDeviceTabs>> {
|
||||||
|
SyncRemoteTabsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncRemoteTabsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncRemoteTabsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<List<SyncDeviceTabs>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<List<SyncDeviceTabs>> create(Ref ref) {
|
||||||
|
return syncRemoteTabs(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncRemoteTabsHash() => r'5040a010e578f2fe395302e7d34a91df1854f8a9';
|
||||||
|
|
||||||
|
@ProviderFor(syncDevices)
|
||||||
|
final syncDevicesProvider = SyncDevicesProvider._();
|
||||||
|
|
||||||
|
final class SyncDevicesProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<SyncDevice>>,
|
||||||
|
List<SyncDevice>,
|
||||||
|
FutureOr<List<SyncDevice>>
|
||||||
|
>
|
||||||
|
with $FutureModifier<List<SyncDevice>>, $FutureProvider<List<SyncDevice>> {
|
||||||
|
SyncDevicesProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncDevicesProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncDevicesHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<List<SyncDevice>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<List<SyncDevice>> create(Ref ref) {
|
||||||
|
return syncDevices(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncDevicesHash() => r'6249b4891f95fc6f8e2f70f1555aff0e6356ccad';
|
||||||
|
|
||||||
|
@ProviderFor(syncEvent)
|
||||||
|
final syncEventProvider = SyncEventProvider._();
|
||||||
|
|
||||||
|
final class SyncEventProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<(SyncEvent?, String?)>,
|
||||||
|
(SyncEvent?, String?),
|
||||||
|
FutureOr<(SyncEvent?, String?)>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<(SyncEvent?, String?)>,
|
||||||
|
$FutureProvider<(SyncEvent?, String?)> {
|
||||||
|
SyncEventProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'syncEventProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$syncEventHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<(SyncEvent?, String?)> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<(SyncEvent?, String?)> create(Ref ref) {
|
||||||
|
return syncEvent(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$syncEventHash() => r'c84d251502578779f66e17ee79e353ffa3b23e1a';
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||||
|
import 'package:weblibre/core/providers/format.dart';
|
||||||
|
import 'package:weblibre/features/qr_scanner/presentation/dialogs/qr_scanner_dialog.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
class SyncSettingsScreen extends HookConsumerWidget {
|
||||||
|
const SyncSettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final syncInfo = ref.watch(
|
||||||
|
syncRepositoryProvider.select((value) => value.value?.account),
|
||||||
|
);
|
||||||
|
|
||||||
|
final generalSettings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
final syncStarted = ref.watch(
|
||||||
|
syncEventProvider.select(
|
||||||
|
(value) => value.isLoading || value.value?.$1 == SyncEvent.started,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final isSyncing = syncStarted || syncInfo?.syncing == true;
|
||||||
|
|
||||||
|
final syncText = useMemoized(() {
|
||||||
|
if (isSyncing) {
|
||||||
|
return 'Synchronization in progress';
|
||||||
|
}
|
||||||
|
|
||||||
|
final timestamp = syncInfo?.lastSyncedAt;
|
||||||
|
if (timestamp == null || timestamp <= 0) {
|
||||||
|
return 'Never synced';
|
||||||
|
}
|
||||||
|
|
||||||
|
final date = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||||
|
final formattedDate = ref
|
||||||
|
.read(formatProvider.notifier)
|
||||||
|
.fullDateTime(date.toLocal());
|
||||||
|
|
||||||
|
return 'Last synced: $formattedDate';
|
||||||
|
}, [syncInfo, isSyncing]);
|
||||||
|
|
||||||
|
final syncController = useAnimationController(
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (isSyncing) {
|
||||||
|
unawaited(syncController.repeat());
|
||||||
|
} else {
|
||||||
|
syncController.stop();
|
||||||
|
syncController.reset();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [isSyncing]);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Firefox Sync')),
|
||||||
|
body: SafeArea(
|
||||||
|
child: FadingScroll(
|
||||||
|
fadingSize: 25,
|
||||||
|
builder: (context, controller) {
|
||||||
|
return ListView(
|
||||||
|
controller: controller,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
children: [
|
||||||
|
const SettingSection(name: 'Account'),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.account_circle_outlined),
|
||||||
|
title: Text(syncInfo?.email ?? 'Not signed in'),
|
||||||
|
subtitle: Text(
|
||||||
|
syncInfo?.needsReauth == true
|
||||||
|
? 'Authentication expired. Sign in again to continue syncing.'
|
||||||
|
: syncInfo?.authenticated == true
|
||||||
|
? (syncInfo?.displayName ?? 'Signed in')
|
||||||
|
: 'Sign in to synchronize tabs, bookmarks, and history',
|
||||||
|
),
|
||||||
|
trailing: syncInfo?.authenticated == true
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.logout),
|
||||||
|
tooltip: 'Sign Out',
|
||||||
|
onPressed: isSyncing
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final confirmed =
|
||||||
|
await _showSignOutConfirmation(context);
|
||||||
|
if (confirmed == true) {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.signOut();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const Icon(Icons.login),
|
||||||
|
onTap: (syncInfo?.authenticated == true || isSyncing)
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.signIn();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (syncInfo?.authenticated != true && !isSyncing)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.qr_code_scanner),
|
||||||
|
title: const Text('Scan QR Code to pair'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Scan a QR code from firefox.com/pair on desktop',
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
final barcode = await showDialog<Barcode>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const QrScannerDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final code = barcode?.code;
|
||||||
|
if (code == null || code.isEmpty) return;
|
||||||
|
|
||||||
|
final uri = Uri.tryParse(code);
|
||||||
|
if (uri == null || !uri.hasScheme) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showErrorMessage(
|
||||||
|
context,
|
||||||
|
'Invalid QR code: not a valid URL',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.signInWithPairing(code);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (syncInfo?.authenticated == true)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.devices),
|
||||||
|
title: const Text('Device Name'),
|
||||||
|
subtitle: ref
|
||||||
|
.watch(syncDeviceNameProvider)
|
||||||
|
.when(
|
||||||
|
data: (name) => Text(name ?? 'Unknown'),
|
||||||
|
loading: () => const Text('Loading...'),
|
||||||
|
error: (_, _) => const Text('Unknown'),
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.edit_outlined),
|
||||||
|
onTap: isSyncing
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final currentName = ref
|
||||||
|
.read(syncRepositoryProvider)
|
||||||
|
.value
|
||||||
|
?.deviceName;
|
||||||
|
|
||||||
|
if (currentName != null && context.mounted) {
|
||||||
|
await _showDeviceNameDialog(
|
||||||
|
context,
|
||||||
|
currentName: currentName,
|
||||||
|
onSave: (newName) {
|
||||||
|
return ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.setDeviceName(newName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SettingSection(name: 'Synchronization'),
|
||||||
|
ListTile(
|
||||||
|
leading: RotationTransition(
|
||||||
|
turns: Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: -1,
|
||||||
|
).animate(syncController),
|
||||||
|
child: const Icon(Icons.sync),
|
||||||
|
),
|
||||||
|
title: const Text('Sync Now'),
|
||||||
|
subtitle: Text(syncText),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: isSyncing
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.syncNow();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: const Text('Sync History'),
|
||||||
|
value: _engineEnabled(syncInfo, SyncEngineValue.history),
|
||||||
|
onChanged: (syncInfo == null || isSyncing)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.setEngineEnabled(SyncEngineValue.history, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: const Text('Sync Bookmarks'),
|
||||||
|
value: _engineEnabled(syncInfo, SyncEngineValue.bookmarks),
|
||||||
|
onChanged: (syncInfo == null || isSyncing)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.setEngineEnabled(
|
||||||
|
SyncEngineValue.bookmarks,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: const Text('Sync Open Tabs'),
|
||||||
|
value: _engineEnabled(syncInfo, SyncEngineValue.tabs),
|
||||||
|
onChanged: (syncInfo == null || isSyncing)
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.setEngineEnabled(SyncEngineValue.tabs, value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SettingSection(name: 'Server Overrides'),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('FxA Server Override'),
|
||||||
|
subtitle: Text(
|
||||||
|
generalSettings.syncServerOverride.isEmpty
|
||||||
|
? 'Default Mozilla server'
|
||||||
|
: generalSettings.syncServerOverride,
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.edit_outlined),
|
||||||
|
onTap: isSyncing
|
||||||
|
? null
|
||||||
|
: () => _showTextSettingDialog(
|
||||||
|
context,
|
||||||
|
title: 'FxA Server Override',
|
||||||
|
initialValue: generalSettings.syncServerOverride,
|
||||||
|
hint: 'https://accounts.firefox.com',
|
||||||
|
onSave: (value) {
|
||||||
|
return ref
|
||||||
|
.read(
|
||||||
|
saveGeneralSettingsControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.save(
|
||||||
|
(current) => current.copyWith
|
||||||
|
.syncServerOverride(value.trim()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Sync Token Server Override'),
|
||||||
|
subtitle: Text(
|
||||||
|
generalSettings.syncTokenServerOverride.isEmpty
|
||||||
|
? 'Automatic from FxA server'
|
||||||
|
: generalSettings.syncTokenServerOverride,
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.edit_outlined),
|
||||||
|
onTap: isSyncing
|
||||||
|
? null
|
||||||
|
: () => _showTextSettingDialog(
|
||||||
|
context,
|
||||||
|
title: 'Sync Token Server Override',
|
||||||
|
initialValue: generalSettings.syncTokenServerOverride,
|
||||||
|
hint:
|
||||||
|
'https://token.services.mozilla.com/1.0/sync/1.5',
|
||||||
|
onSave: (value) {
|
||||||
|
return ref
|
||||||
|
.read(
|
||||||
|
saveGeneralSettingsControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.save(
|
||||||
|
(current) => current.copyWith
|
||||||
|
.syncTokenServerOverride(value.trim()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
'Restart the app after changing server overrides.',
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _engineEnabled(SyncAccountInfo? info, SyncEngineValue engine) {
|
||||||
|
final engines = info?.engines;
|
||||||
|
if (engines == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final status in engines) {
|
||||||
|
if (status.engine == engine) {
|
||||||
|
return status.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool?> _showSignOutConfirmation(BuildContext context) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Sign out?'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to sign out of Firefox Sync?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Sign Out'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showDeviceNameDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
required String currentName,
|
||||||
|
required Future<bool> Function(String name) onSave,
|
||||||
|
}) async {
|
||||||
|
final controller = TextEditingController(text: currentName);
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Device Name'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: const InputDecoration(hintText: 'Enter device name'),
|
||||||
|
autofocus: true,
|
||||||
|
inputFormatters: [LengthLimitingTextInputFormatter(128)],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final newName = controller.text.trim();
|
||||||
|
if (newName.isEmpty) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Device name cannot be empty'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final success = await onSave(newName).catchError((_) => false);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Failed to update device name'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _showTextSettingDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String initialValue,
|
||||||
|
required String hint,
|
||||||
|
required Future<void> Function(String value) onSave,
|
||||||
|
}) async {
|
||||||
|
final controller = TextEditingController(text: initialValue);
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(hintText: hint),
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final value = controller.text.trim();
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final uri = Uri.tryParse(value);
|
||||||
|
if (uri == null || uri.scheme != 'https') {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Must be a valid HTTPS URL'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await onSave(controller.text);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Save'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,6 +89,8 @@ class GeneralSettings with FastEquatable {
|
|||||||
final bool tabListShowFavicons;
|
final bool tabListShowFavicons;
|
||||||
final bool quickTabSwitcherShowTitles;
|
final bool quickTabSwitcherShowTitles;
|
||||||
final bool drawerGestureEnabled;
|
final bool drawerGestureEnabled;
|
||||||
|
final String syncServerOverride;
|
||||||
|
final String syncTokenServerOverride;
|
||||||
|
|
||||||
GeneralSettings({
|
GeneralSettings({
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
@@ -120,6 +122,8 @@ class GeneralSettings with FastEquatable {
|
|||||||
required this.tabListShowFavicons,
|
required this.tabListShowFavicons,
|
||||||
required this.quickTabSwitcherShowTitles,
|
required this.quickTabSwitcherShowTitles,
|
||||||
required this.drawerGestureEnabled,
|
required this.drawerGestureEnabled,
|
||||||
|
required this.syncServerOverride,
|
||||||
|
required this.syncTokenServerOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
GeneralSettings.withDefaults({
|
GeneralSettings.withDefaults({
|
||||||
@@ -152,6 +156,8 @@ class GeneralSettings with FastEquatable {
|
|||||||
bool? tabListShowFavicons,
|
bool? tabListShowFavicons,
|
||||||
bool? quickTabSwitcherShowTitles,
|
bool? quickTabSwitcherShowTitles,
|
||||||
bool? drawerGestureEnabled,
|
bool? drawerGestureEnabled,
|
||||||
|
String? syncServerOverride,
|
||||||
|
String? syncTokenServerOverride,
|
||||||
}) : themeMode = themeMode ?? ThemeMode.dark,
|
}) : themeMode = themeMode ?? ThemeMode.dark,
|
||||||
enableReadability = enableReadability ?? true,
|
enableReadability = enableReadability ?? true,
|
||||||
enforceReadability = enforceReadability ?? false,
|
enforceReadability = enforceReadability ?? false,
|
||||||
@@ -184,7 +190,9 @@ class GeneralSettings with FastEquatable {
|
|||||||
allowClipboardAccess = allowClipboardAccess ?? true,
|
allowClipboardAccess = allowClipboardAccess ?? true,
|
||||||
tabListShowFavicons = tabListShowFavicons ?? false,
|
tabListShowFavicons = tabListShowFavicons ?? false,
|
||||||
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
|
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
|
||||||
drawerGestureEnabled = drawerGestureEnabled ?? false;
|
drawerGestureEnabled = drawerGestureEnabled ?? false,
|
||||||
|
syncServerOverride = syncServerOverride ?? '',
|
||||||
|
syncTokenServerOverride = syncTokenServerOverride ?? '';
|
||||||
|
|
||||||
factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
|
factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
|
||||||
_$GeneralSettingsFromJson(json);
|
_$GeneralSettingsFromJson(json);
|
||||||
@@ -222,5 +230,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
tabListShowFavicons,
|
tabListShowFavicons,
|
||||||
quickTabSwitcherShowTitles,
|
quickTabSwitcherShowTitles,
|
||||||
drawerGestureEnabled,
|
drawerGestureEnabled,
|
||||||
|
syncServerOverride,
|
||||||
|
syncTokenServerOverride,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
|
|
||||||
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled);
|
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled);
|
||||||
|
|
||||||
|
GeneralSettings syncServerOverride(String syncServerOverride);
|
||||||
|
|
||||||
|
GeneralSettings syncTokenServerOverride(String syncTokenServerOverride);
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
||||||
///
|
///
|
||||||
@@ -114,6 +118,8 @@ abstract class _$GeneralSettingsCWProxy {
|
|||||||
bool tabListShowFavicons,
|
bool tabListShowFavicons,
|
||||||
bool quickTabSwitcherShowTitles,
|
bool quickTabSwitcherShowTitles,
|
||||||
bool drawerGestureEnabled,
|
bool drawerGestureEnabled,
|
||||||
|
String syncServerOverride,
|
||||||
|
String syncTokenServerOverride,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +251,14 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled) =>
|
GeneralSettings drawerGestureEnabled(bool drawerGestureEnabled) =>
|
||||||
call(drawerGestureEnabled: drawerGestureEnabled);
|
call(drawerGestureEnabled: drawerGestureEnabled);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeneralSettings syncServerOverride(String syncServerOverride) =>
|
||||||
|
call(syncServerOverride: syncServerOverride);
|
||||||
|
|
||||||
|
@override
|
||||||
|
GeneralSettings syncTokenServerOverride(String syncTokenServerOverride) =>
|
||||||
|
call(syncTokenServerOverride: syncTokenServerOverride);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
|
||||||
@@ -283,6 +297,8 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
Object? tabListShowFavicons = const $CopyWithPlaceholder(),
|
Object? tabListShowFavicons = const $CopyWithPlaceholder(),
|
||||||
Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(),
|
Object? quickTabSwitcherShowTitles = const $CopyWithPlaceholder(),
|
||||||
Object? drawerGestureEnabled = const $CopyWithPlaceholder(),
|
Object? drawerGestureEnabled = const $CopyWithPlaceholder(),
|
||||||
|
Object? syncServerOverride = const $CopyWithPlaceholder(),
|
||||||
|
Object? syncTokenServerOverride = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return GeneralSettings(
|
return GeneralSettings(
|
||||||
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
|
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
|
||||||
@@ -455,6 +471,18 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
|||||||
? _value.drawerGestureEnabled
|
? _value.drawerGestureEnabled
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: drawerGestureEnabled as bool,
|
: drawerGestureEnabled as bool,
|
||||||
|
syncServerOverride:
|
||||||
|
syncServerOverride == const $CopyWithPlaceholder() ||
|
||||||
|
syncServerOverride == null
|
||||||
|
? _value.syncServerOverride
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: syncServerOverride as String,
|
||||||
|
syncTokenServerOverride:
|
||||||
|
syncTokenServerOverride == const $CopyWithPlaceholder() ||
|
||||||
|
syncTokenServerOverride == null
|
||||||
|
? _value.syncTokenServerOverride
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: syncTokenServerOverride as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,6 +562,8 @@ GeneralSettings _$GeneralSettingsFromJson(
|
|||||||
tabListShowFavicons: json['tabListShowFavicons'] as bool?,
|
tabListShowFavicons: json['tabListShowFavicons'] as bool?,
|
||||||
quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?,
|
quickTabSwitcherShowTitles: json['quickTabSwitcherShowTitles'] as bool?,
|
||||||
drawerGestureEnabled: json['drawerGestureEnabled'] as bool?,
|
drawerGestureEnabled: json['drawerGestureEnabled'] as bool?,
|
||||||
|
syncServerOverride: json['syncServerOverride'] as String?,
|
||||||
|
syncTokenServerOverride: json['syncTokenServerOverride'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$GeneralSettingsToJson(
|
Map<String, dynamic> _$GeneralSettingsToJson(
|
||||||
@@ -577,6 +607,8 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
|||||||
'tabListShowFavicons': instance.tabListShowFavicons,
|
'tabListShowFavicons': instance.tabListShowFavicons,
|
||||||
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
|
'quickTabSwitcherShowTitles': instance.quickTabSwitcherShowTitles,
|
||||||
'drawerGestureEnabled': instance.drawerGestureEnabled,
|
'drawerGestureEnabled': instance.drawerGestureEnabled,
|
||||||
|
'syncServerOverride': instance.syncServerOverride,
|
||||||
|
'syncTokenServerOverride': instance.syncTokenServerOverride,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ThemeModeEnumMap = {
|
const _$ThemeModeEnumMap = {
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
|||||||
key: ValueKey(file.path),
|
key: ValueKey(file.path),
|
||||||
title: Text(profileName),
|
title: Text(profileName),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
ref
|
ref.read(formatProvider.notifier).fullDateTime(dateTime),
|
||||||
.read(formatProvider.notifier)
|
|
||||||
.fullDateTimeWithTimezone(dateTime),
|
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await RestoreProfileRoute(
|
await RestoreProfileRoute(
|
||||||
|
|||||||
@@ -155,6 +155,14 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
|
|||||||
DriftSqlType.bool,
|
DriftSqlType.bool,
|
||||||
db.typeMapping,
|
db.typeMapping,
|
||||||
),
|
),
|
||||||
|
'syncServerOverride': settings['syncServerOverride']?.readAs(
|
||||||
|
DriftSqlType.string,
|
||||||
|
db.typeMapping,
|
||||||
|
),
|
||||||
|
'syncTokenServerOverride': settings['syncTokenServerOverride']?.readAs(
|
||||||
|
DriftSqlType.string,
|
||||||
|
db.typeMapping,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$generalSettingsRepositoryHash() =>
|
String _$generalSettingsRepositoryHash() =>
|
||||||
r'33730ccb09471c1a7c187bd19760110f524e055b';
|
r'ed5bca200b840e0802980b85a0c0f682d7b24a8c';
|
||||||
|
|
||||||
abstract class _$GeneralSettingsRepository
|
abstract class _$GeneralSettingsRepository
|
||||||
extends $StreamNotifier<GeneralSettings> {
|
extends $StreamNotifier<GeneralSettings> {
|
||||||
|
|||||||
@@ -147,13 +147,13 @@ class FeedArticleScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Text(
|
Text(
|
||||||
'Published: ${hasArticleCreated ? ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.created!) : 'N/A'}',
|
'Published: ${hasArticleCreated ? ref.read(formatProvider.notifier).fullDateTime(article.created!) : 'N/A'}',
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
?.copyWith(fontStyle: FontStyle.italic),
|
?.copyWith(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
if (hasArticleUpdated)
|
if (hasArticleUpdated)
|
||||||
Text(
|
Text(
|
||||||
'Updated: ${ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.updated!)}',
|
'Updated: ${ref.read(formatProvider.notifier).fullDateTime(article.updated!)}',
|
||||||
style: Theme.of(context).textTheme.bodySmall
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
?.copyWith(fontStyle: FontStyle.italic),
|
?.copyWith(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:background_fetch/background_fetch.dart';
|
import 'package:background_fetch/background_fetch.dart';
|
||||||
@@ -101,6 +102,9 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
final engineSettings = await ref
|
final engineSettings = await ref
|
||||||
.read(engineSettingsRepositoryProvider.notifier)
|
.read(engineSettingsRepositoryProvider.notifier)
|
||||||
.fetchSettings();
|
.fetchSettings();
|
||||||
|
final generalSettings = await ref
|
||||||
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
|
.fetchSettings();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await GeckoBrowserService().initialize(
|
await GeckoBrowserService().initialize(
|
||||||
@@ -108,6 +112,8 @@ class _MainWidget extends HookConsumerWidget {
|
|||||||
kDebugMode ? LogLevel.debug : LogLevel.warn,
|
kDebugMode ? LogLevel.debug : LogLevel.warn,
|
||||||
engineSettings.contentBlocking,
|
engineSettings.contentBlocking,
|
||||||
engineSettings.addonCollection,
|
engineSettings.addonCollection,
|
||||||
|
generalSettings.syncServerOverride,
|
||||||
|
generalSettings.syncTokenServerOverride,
|
||||||
);
|
);
|
||||||
} on PlatformException catch (e, s) {
|
} on PlatformException catch (e, s) {
|
||||||
logger.e(
|
logger.e(
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/providers/router.dart';
|
import 'package:weblibre/core/providers/router.dart';
|
||||||
import 'package:weblibre/domain/services/app_initialization.dart';
|
import 'package:weblibre/domain/services/app_initialization.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class MainApp extends HookConsumerWidget {
|
class MainApp extends HookConsumerWidget {
|
||||||
final ThemeData? theme;
|
final ThemeData? theme;
|
||||||
@@ -72,6 +75,9 @@ class MainApp extends HookConsumerWidget {
|
|||||||
darkTheme: darkTheme,
|
darkTheme: darkTheme,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
routerConfig: router.value,
|
routerConfig: router.value,
|
||||||
|
builder: (context, child) {
|
||||||
|
return _SyncEventListener(child: child ?? const SizedBox.shrink());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onFailure: (errorMessage) {
|
onFailure: (errorMessage) {
|
||||||
@@ -99,3 +105,41 @@ class MainApp extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SyncEventListener extends ConsumerWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _SyncEventListener({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
ref.listen(syncEventProvider, (previous, next) {
|
||||||
|
if (next.isLoading || !next.hasValue) return;
|
||||||
|
|
||||||
|
final event = next.value;
|
||||||
|
if (event == null) return;
|
||||||
|
|
||||||
|
final (syncEvent, syncError) = event;
|
||||||
|
|
||||||
|
switch (syncEvent) {
|
||||||
|
// case SyncEvent.completed:
|
||||||
|
// ui_helper.showInfoMessage(
|
||||||
|
// context,
|
||||||
|
// 'Synchronization complete',
|
||||||
|
// duration: const Duration(seconds: 2),
|
||||||
|
// );
|
||||||
|
case SyncEvent.error:
|
||||||
|
ui_helper.showErrorMessage(
|
||||||
|
context,
|
||||||
|
syncError ?? 'Synchronization failed',
|
||||||
|
);
|
||||||
|
case SyncEvent.started:
|
||||||
|
case SyncEvent.completed:
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ SnackBar _createFloatingSnackBar({
|
|||||||
required Widget content,
|
required Widget content,
|
||||||
Color? backgroundColor,
|
Color? backgroundColor,
|
||||||
SnackBarAction? action,
|
SnackBarAction? action,
|
||||||
Duration duration = const Duration(seconds: 4),
|
required Duration duration,
|
||||||
bool persist = false,
|
required bool persist,
|
||||||
}) {
|
}) {
|
||||||
return SnackBar(
|
return SnackBar(
|
||||||
content: content,
|
content: content,
|
||||||
@@ -43,34 +43,69 @@ SnackBar _createFloatingSnackBar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showErrorMessage(BuildContext context, String message) {
|
void showErrorMessage(
|
||||||
|
BuildContext context,
|
||||||
|
String message, {
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool persist = false,
|
||||||
|
}) {
|
||||||
final snackBar = _createFloatingSnackBar(
|
final snackBar = _createFloatingSnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
message,
|
message,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).colorScheme.onError,
|
backgroundColor: Theme.of(context).colorScheme.onError,
|
||||||
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInfoMessage(BuildContext context, String message) {
|
void showInfoMessage(
|
||||||
final snackBar = _createFloatingSnackBar(content: Text(message));
|
BuildContext context,
|
||||||
|
String message, {
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool persist = false,
|
||||||
|
}) {
|
||||||
|
final snackBar = _createFloatingSnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showOpenedTabsFromAnotherDeviceMessage(
|
||||||
|
BuildContext context,
|
||||||
|
int openedTabs, {
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool persist = false,
|
||||||
|
}) {
|
||||||
|
if (openedTabs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = openedTabs == 1
|
||||||
|
? 'Opened 1 tab received from another device'
|
||||||
|
: 'Opened $openedTabs tabs received from another device';
|
||||||
|
|
||||||
|
showInfoMessage(context, message, duration: duration, persist: persist);
|
||||||
|
}
|
||||||
|
|
||||||
void showTabBackButtonMessage(
|
void showTabBackButtonMessage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
int tabCount,
|
int tabCount,
|
||||||
Duration duration,
|
Duration duration, {
|
||||||
) {
|
bool persist = false,
|
||||||
|
}) {
|
||||||
final snackbar = _createFloatingSnackBar(
|
final snackbar = _createFloatingSnackBar(
|
||||||
content: (tabCount > 1)
|
content: (tabCount > 1)
|
||||||
? const Text('Navigate BACK again to close current tab')
|
? const Text('Navigate BACK again to close current tab')
|
||||||
: const Text('Navigate BACK again to exit app'),
|
: const Text('Navigate BACK again to exit app'),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
@@ -83,6 +118,7 @@ void showTabOpenedMessage(
|
|||||||
String? tabName,
|
String? tabName,
|
||||||
void Function()? onShow,
|
void Function()? onShow,
|
||||||
Duration duration = const Duration(seconds: 3),
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool persist = false,
|
||||||
}) {
|
}) {
|
||||||
final message = switch (tabName.whenNotEmpty) {
|
final message = switch (tabName.whenNotEmpty) {
|
||||||
String() => "New tab '$tabName' opened in background",
|
String() => "New tab '$tabName' opened in background",
|
||||||
@@ -95,6 +131,7 @@ void showTabOpenedMessage(
|
|||||||
(onPressed) => SnackBarAction(label: 'Show', onPressed: onPressed),
|
(onPressed) => SnackBarAction(label: 'Show', onPressed: onPressed),
|
||||||
),
|
),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
@@ -104,6 +141,7 @@ Future<void> showSuggestNewTabMessage(
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required void Function(String? searchText) onAdd,
|
required void Function(String? searchText) onAdd,
|
||||||
Duration duration = const Duration(seconds: 3),
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool persist = false,
|
||||||
}) async {
|
}) async {
|
||||||
final clipboardUrl = await tryGetUriFromClipboard();
|
final clipboardUrl = await tryGetUriFromClipboard();
|
||||||
|
|
||||||
@@ -117,6 +155,7 @@ Future<void> showSuggestNewTabMessage(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -130,6 +169,7 @@ void showTabSwitchMessage(
|
|||||||
String? tabName,
|
String? tabName,
|
||||||
void Function()? onSwitch,
|
void Function()? onSwitch,
|
||||||
Duration duration = const Duration(seconds: 3),
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool persist = false,
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
|
||||||
@@ -144,6 +184,7 @@ void showTabSwitchMessage(
|
|||||||
(onPressed) => SnackBarAction(label: 'Switch', onPressed: onPressed),
|
(onPressed) => SnackBarAction(label: 'Switch', onPressed: onPressed),
|
||||||
),
|
),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
@@ -179,6 +220,7 @@ void showTabUndoClose(
|
|||||||
VoidCallback onUndo, {
|
VoidCallback onUndo, {
|
||||||
int count = 1,
|
int count = 1,
|
||||||
Duration duration = const Duration(seconds: 3),
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
bool persist = false,
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
|
||||||
@@ -188,6 +230,7 @@ void showTabUndoClose(
|
|||||||
: const Text('Tab closed'),
|
: const Text('Tab closed'),
|
||||||
action: SnackBarAction(label: 'Undo', onPressed: onUndo),
|
action: SnackBarAction(label: 'Undo', onPressed: onUndo),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
@@ -197,6 +240,7 @@ void showDismissOverrideMessage(
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
VoidCallback onDismiss, {
|
VoidCallback onDismiss, {
|
||||||
Duration duration = const Duration(seconds: 4),
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
bool persist = false,
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
|
|
||||||
@@ -204,6 +248,7 @@ void showDismissOverrideMessage(
|
|||||||
content: const Text('Hiding disabled by site'),
|
content: const Text('Hiding disabled by site'),
|
||||||
action: SnackBarAction(label: 'Dismiss', onPressed: onDismiss),
|
action: SnackBarAction(label: 'Dismiss', onPressed: onDismiss),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
persist: persist,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ android {
|
|||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: "org.mozilla.telemetry", module: "glean-native"
|
exclude group: "org.mozilla.telemetry", module: "glean-native"
|
||||||
|
// Exclude standalone tink to avoid duplicate classes with tink-android
|
||||||
|
exclude group: "com.google.crypto.tink", module: "tink"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the Glean from GeckoView.
|
// Select the Glean from GeckoView.
|
||||||
@@ -114,6 +116,8 @@ dependencies {
|
|||||||
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-accounts:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-accounts-push:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-customtabs:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-customtabs:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
|
||||||
@@ -122,6 +126,7 @@ dependencies {
|
|||||||
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-syncedtabs:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
|
||||||
@@ -131,6 +136,8 @@ dependencies {
|
|||||||
implementation "org.mozilla.components:feature-intent:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:feature-intent:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
|
||||||
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
|
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:service-firefox-accounts:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:support-appservices:$mozillaComponentsVersion"
|
||||||
|
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2025 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package eu.weblibre.flutter_mozilla_components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object ActiveProfile {
|
||||||
|
@Volatile
|
||||||
|
var prefix: String? = null
|
||||||
|
|
||||||
|
/** SharedPreference names used by mozilla-components FxA/sync that need profile isolation */
|
||||||
|
val FXA_SHARED_PREFERENCE_NAMES = setOf(
|
||||||
|
"fxaAppState", // FxA account state (SharedPrefAccountStorage)
|
||||||
|
"fxaStatePrefAC", // Sentinel flag for SecureAbove22 account state presence
|
||||||
|
"fxaStateAC_kp_pre_m", // SecureAbove22 encrypted account state (API < 23 fallback)
|
||||||
|
"fxaStateAC_kp_post_m", // SecureAbove22 encrypted account state (API >= 23)
|
||||||
|
"fxa_abnormalities", // Tracks FxA account abnormalities
|
||||||
|
"mozac_feature_accounts_push", // Push subscription scope + verification state
|
||||||
|
"SyncAuthInfoCache", // Cached sync auth tokens
|
||||||
|
"FxaDeviceSettingsCache", // Cached device settings (ID, name, type)
|
||||||
|
"syncEngines", // Per-engine enabled/disabled state
|
||||||
|
"syncPrefs", // Last-synced timestamp + persisted sync state
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the active profile prefix from disk.
|
||||||
|
* Called in Application.onCreate() to handle cold-start WorkManager scenarios.
|
||||||
|
*/
|
||||||
|
fun resolveFromDisk(context: Context) {
|
||||||
|
val profileFile = File(context.filesDir, PwaConstants.CURRENT_PROFILE_FILE)
|
||||||
|
if (!profileFile.exists()) return
|
||||||
|
val uuid = profileFile.readText().trim().ifEmpty { return }
|
||||||
|
val relativePath = "${PwaConstants.PROFILES_DIR_NAME}/${PwaConstants.PROFILE_DIR_PREFIX}$uuid"
|
||||||
|
prefix = File(relativePath).name
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -37,6 +37,8 @@ import io.flutter.Log
|
|||||||
import mozilla.components.browser.state.state.WebExtensionState
|
import mozilla.components.browser.state.state.WebExtensionState
|
||||||
import mozilla.components.browser.thumbnails.BrowserThumbnails
|
import mozilla.components.browser.thumbnails.BrowserThumbnails
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
|
import mozilla.components.feature.accounts.FxaCapability
|
||||||
|
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
||||||
import mozilla.components.feature.app.links.AppLinksFeature
|
import mozilla.components.feature.app.links.AppLinksFeature
|
||||||
import mozilla.components.feature.downloads.DownloadsFeature
|
import mozilla.components.feature.downloads.DownloadsFeature
|
||||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||||
@@ -85,6 +87,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
|
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
|
||||||
|
|
||||||
private val webAuthnFeature = ViewBoundFeatureWrapper<WebAuthnFeature>()
|
private val webAuthnFeature = ViewBoundFeatureWrapper<WebAuthnFeature>()
|
||||||
|
private val fxaWebChannelFeature = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
|
||||||
|
|
||||||
private var pictureInPictureFeature: PictureInPictureFeature? = null
|
private var pictureInPictureFeature: PictureInPictureFeature? = null
|
||||||
|
|
||||||
@@ -450,6 +453,19 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
view = view
|
view = view
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fxaWebChannelFeature.set(
|
||||||
|
feature = FxaWebChannelFeature(
|
||||||
|
customTabSessionId = sessionId,
|
||||||
|
runtime = components.core.engine,
|
||||||
|
store = components.core.store,
|
||||||
|
accountManager = components.backgroundServices.accountManager,
|
||||||
|
serverConfig = components.backgroundServices.serverConfig,
|
||||||
|
fxaCapabilities = setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC),
|
||||||
|
),
|
||||||
|
owner = this,
|
||||||
|
view = view,
|
||||||
|
)
|
||||||
|
|
||||||
readerViewFeature.set(
|
readerViewFeature.set(
|
||||||
feature = ReaderViewIntegration(
|
feature = ReaderViewIntegration(
|
||||||
profileContext,
|
profileContext,
|
||||||
|
|||||||
+29
-3
@@ -9,6 +9,7 @@ package eu.weblibre.flutter_mozilla_components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import eu.weblibre.flutter_mozilla_components.components.Core
|
import eu.weblibre.flutter_mozilla_components.components.Core
|
||||||
|
import eu.weblibre.flutter_mozilla_components.components.BackgroundServices
|
||||||
import eu.weblibre.flutter_mozilla_components.components.Events
|
import eu.weblibre.flutter_mozilla_components.components.Events
|
||||||
import eu.weblibre.flutter_mozilla_components.components.Features
|
import eu.weblibre.flutter_mozilla_components.components.Features
|
||||||
import eu.weblibre.flutter_mozilla_components.components.Search
|
import eu.weblibre.flutter_mozilla_components.components.Search
|
||||||
@@ -19,6 +20,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
|||||||
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
|
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
@@ -38,14 +40,38 @@ class Components(val profileApplicationContext: ProfileContext,
|
|||||||
val logLevel: Log.Priority,
|
val logLevel: Log.Priority,
|
||||||
val contentBlocking: ContentBlocking,
|
val contentBlocking: ContentBlocking,
|
||||||
val addonCollection: AddonCollection?,
|
val addonCollection: AddonCollection?,
|
||||||
|
val fxaServerOverride: String?,
|
||||||
|
val syncTokenServerOverride: String?,
|
||||||
val addonEvents: GeckoAddonEvents,
|
val addonEvents: GeckoAddonEvents,
|
||||||
private val tabContentEvents: GeckoTabContentEvents,
|
private val tabContentEvents: GeckoTabContentEvents,
|
||||||
private val extensionEvents: BrowserExtensionEvents
|
private val extensionEvents: BrowserExtensionEvents,
|
||||||
|
private val syncStateEvents: GeckoSyncStateEvents?,
|
||||||
) {
|
) {
|
||||||
val core by lazy { Core(profileApplicationContext, this, flutterEvents, extensionEvents) }
|
val core by lazy { Core(profileApplicationContext, this, flutterEvents, extensionEvents) }
|
||||||
|
val backgroundServices by lazy {
|
||||||
|
BackgroundServices(
|
||||||
|
context = profileApplicationContext,
|
||||||
|
browserStore = lazy { core.store },
|
||||||
|
historyStorage = core.lazyHistoryStorage,
|
||||||
|
bookmarkStorage = core.lazyBookmarksStorage,
|
||||||
|
remoteTabsStorage = core.lazyRemoteTabsStorage,
|
||||||
|
fxaServerOverride = fxaServerOverride,
|
||||||
|
syncTokenServerOverride = syncTokenServerOverride,
|
||||||
|
syncStateEvents = syncStateEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
val events by lazy { Events(flutterEvents) }
|
val events by lazy { Events(flutterEvents) }
|
||||||
val useCases by lazy { UseCases(profileApplicationContext, core.engine, core.store, core.webAppShortcutManager) }
|
val useCases by lazy { UseCases(profileApplicationContext, core.engine, core.store, core.webAppShortcutManager) }
|
||||||
val services by lazy { Services(profileApplicationContext, core.store, useCases.tabsUseCases) }
|
val services by lazy {
|
||||||
|
Services(
|
||||||
|
profileApplicationContext,
|
||||||
|
core.store,
|
||||||
|
useCases.tabsUseCases,
|
||||||
|
backgroundServices.accountManager,
|
||||||
|
core.engine,
|
||||||
|
backgroundServices.serverConfig,
|
||||||
|
)
|
||||||
|
}
|
||||||
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
|
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
|
||||||
val search by lazy { Search(profileApplicationContext, core, useCases) }
|
val search by lazy { Search(profileApplicationContext, core, useCases) }
|
||||||
|
|
||||||
@@ -68,4 +94,4 @@ class Components(val profileApplicationContext: ProfileContext,
|
|||||||
val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() }
|
val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() }
|
||||||
|
|
||||||
val downloadEstimator: DownloadEstimator by lazy { DownloadEstimator(dateTimeProvider = dateTimeProvider) }
|
val downloadEstimator: DownloadEstimator by lazy { DownloadEstimator(dateTimeProvider = dateTimeProvider) }
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-1
@@ -15,6 +15,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
|||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||||
@@ -26,6 +27,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
||||||
import mozilla.components.browser.state.action.CustomTabListAction
|
import mozilla.components.browser.state.action.CustomTabListAction
|
||||||
import mozilla.components.browser.state.selector.findCustomTab
|
import mozilla.components.browser.state.selector.findCustomTab
|
||||||
@@ -114,9 +116,12 @@ object GlobalComponents {
|
|||||||
addonEvents: GeckoAddonEvents,
|
addonEvents: GeckoAddonEvents,
|
||||||
tabContentEvents: GeckoTabContentEvents,
|
tabContentEvents: GeckoTabContentEvents,
|
||||||
extensionEvents: BrowserExtensionEvents,
|
extensionEvents: BrowserExtensionEvents,
|
||||||
|
syncStateEvents: GeckoSyncStateEvents?,
|
||||||
logLevel: Log.Priority,
|
logLevel: Log.Priority,
|
||||||
contentBlocking: ContentBlocking,
|
contentBlocking: ContentBlocking,
|
||||||
addonCollection: AddonCollection?,
|
addonCollection: AddonCollection?,
|
||||||
|
fxaServerOverride: String?,
|
||||||
|
syncTokenServerOverride: String?,
|
||||||
mode: ComponentsMode = ComponentsMode.FULL,
|
mode: ComponentsMode = ComponentsMode.FULL,
|
||||||
) {
|
) {
|
||||||
Logger.debug("Creating new components")
|
Logger.debug("Creating new components")
|
||||||
@@ -137,18 +142,30 @@ object GlobalComponents {
|
|||||||
logLevel,
|
logLevel,
|
||||||
contentBlocking,
|
contentBlocking,
|
||||||
addonCollection,
|
addonCollection,
|
||||||
|
fxaServerOverride,
|
||||||
|
syncTokenServerOverride,
|
||||||
addonEvents,
|
addonEvents,
|
||||||
tabContentEvents,
|
tabContentEvents,
|
||||||
extensionEvents,
|
extensionEvents,
|
||||||
|
syncStateEvents,
|
||||||
)
|
)
|
||||||
_components = newComponents
|
_components = newComponents
|
||||||
currentMode = mode
|
currentMode = mode
|
||||||
|
|
||||||
|
previousComponents?.let {
|
||||||
|
runCatching {
|
||||||
|
it.backgroundServices.accountManager.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//newComponents.crashReporter.install(applicationContext)
|
//newComponents.crashReporter.install(applicationContext)
|
||||||
|
|
||||||
//Facts.registerProcessor(LogFactProcessor())
|
//Facts.registerProcessor(LogFactProcessor())
|
||||||
|
|
||||||
//RustHttpConfig.setClient(lazy { newComponents.core.client })
|
val megazordNetworkSetup = MegazordSetup.setupMegazordNetwork(
|
||||||
|
context = newComponents.profileApplicationContext,
|
||||||
|
client = lazy { newComponents.core.client },
|
||||||
|
)
|
||||||
|
|
||||||
if (mode == ComponentsMode.FULL) {
|
if (mode == ComponentsMode.FULL) {
|
||||||
newComponents.core.engine.warmUp()
|
newComponents.core.engine.warmUp()
|
||||||
@@ -167,6 +184,12 @@ object GlobalComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode == ComponentsMode.FULL) {
|
if (mode == ComponentsMode.FULL) {
|
||||||
|
if (!megazordNetworkSetup.isCompleted) {
|
||||||
|
runBlocking {
|
||||||
|
megazordNetworkSetup.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val restoreJob = restoreBrowserState(newComponents)
|
val restoreJob = restoreBrowserState(newComponents)
|
||||||
if (previousCustomTabs.isNotEmpty()) {
|
if (previousCustomTabs.isNotEmpty()) {
|
||||||
restoreJob.invokeOnCompletion {
|
restoreJob.invokeOnCompletion {
|
||||||
@@ -215,6 +238,12 @@ object GlobalComponents {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eagerly initialize account manager so sync starts
|
||||||
|
newComponents.backgroundServices.accountManager
|
||||||
|
|
||||||
|
// Start FxA web channel feature for OAuth redirect handling
|
||||||
|
newComponents.services.fxaWebChannelFeature.start()
|
||||||
} else {
|
} else {
|
||||||
restorePreviousCustomTabs()
|
restorePreviousCustomTabs()
|
||||||
}
|
}
|
||||||
@@ -256,9 +285,12 @@ object GlobalComponents {
|
|||||||
addonEvents = GeckoAddonEvents(messenger),
|
addonEvents = GeckoAddonEvents(messenger),
|
||||||
tabContentEvents = GeckoTabContentEvents(messenger),
|
tabContentEvents = GeckoTabContentEvents(messenger),
|
||||||
extensionEvents = BrowserExtensionEvents(messenger),
|
extensionEvents = BrowserExtensionEvents(messenger),
|
||||||
|
syncStateEvents = null,
|
||||||
logLevel = logLevel,
|
logLevel = logLevel,
|
||||||
contentBlocking = contentBlocking,
|
contentBlocking = contentBlocking,
|
||||||
addonCollection = null,
|
addonCollection = null,
|
||||||
|
fxaServerOverride = null,
|
||||||
|
syncTokenServerOverride = null,
|
||||||
mode = ComponentsMode.EXTERNAL,
|
mode = ComponentsMode.EXTERNAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import mozilla.components.concept.fetch.Client
|
||||||
|
import mozilla.components.support.AppServicesInitializer
|
||||||
|
import mozilla.components.support.AppServicesInitializer.Config as AppServicesConfig
|
||||||
|
import mozilla.components.support.rusthttp.RustHttpConfig
|
||||||
|
|
||||||
|
object MegazordSetup {
|
||||||
|
fun setupEarlyMainProcess() {
|
||||||
|
AppServicesInitializer.init(AppServicesConfig(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
fun setupMegazordNetwork(context: Context, client: Lazy<Client>): Deferred<Unit> =
|
||||||
|
GlobalScope.async(Dispatchers.IO) {
|
||||||
|
val isDebuggable =
|
||||||
|
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
|
|
||||||
|
if (isDebuggable) {
|
||||||
|
RustHttpConfig.allowEmulatorLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
|
RustHttpConfig.setClient(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -43,6 +43,7 @@ class ProfileContext(private val base: Context, val relativePath: String) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
ActiveProfile.prefix = profilePrefix
|
||||||
customFilesDir.mkdirs()
|
customFilesDir.mkdirs()
|
||||||
customNoBackupFilesDir.mkdirs()
|
customNoBackupFilesDir.mkdirs()
|
||||||
customObbDir.mkdirs()
|
customObbDir.mkdirs()
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.activities
|
||||||
|
|
||||||
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
|
import mozilla.components.concept.sync.AuthType
|
||||||
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
|
|
||||||
|
class AuthCustomTabActivity : ExternalAppBrowserActivity() {
|
||||||
|
private val accountStateObserver = object : AccountObserver {
|
||||||
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
GlobalComponents.components
|
||||||
|
?.backgroundServices
|
||||||
|
?.accountManager
|
||||||
|
?.register(accountStateObserver, this, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
GlobalComponents.components
|
||||||
|
?.backgroundServices
|
||||||
|
?.accountManager
|
||||||
|
?.unregister(accountStateObserver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
|
||||||
|
import mozilla.components.feature.intent.ext.getSessionId
|
||||||
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
|
|
||||||
|
class AuthIntentReceiverActivity : Activity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "AuthIntentReceiver"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val sourceIntent = intent?.let { Intent(it) } ?: Intent()
|
||||||
|
|
||||||
|
if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val components = GlobalComponents.components
|
||||||
|
if (components == null) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val processed = CustomTabIntentProcessor(
|
||||||
|
components.useCases.customTabsUseCases.add,
|
||||||
|
resources,
|
||||||
|
isPrivate = false,
|
||||||
|
).process(sourceIntent)
|
||||||
|
|
||||||
|
if (processed) {
|
||||||
|
val sessionId = sourceIntent.getSessionId() ?: components.core.store.state.customTabs.lastOrNull()?.id
|
||||||
|
if (sessionId != null) {
|
||||||
|
val authIntent = ExternalAppBrowserActivity
|
||||||
|
.createIntent(this, sessionId)
|
||||||
|
.setClassName(this, AuthCustomTabActivity::class.java.name)
|
||||||
|
startActivity(authIntent)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Auth intent processed but no custom tab session id found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Auth custom tab intent was not processed")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -34,7 +34,7 @@ import mozilla.components.support.base.log.logger.Logger
|
|||||||
*
|
*
|
||||||
* Uses an empty taskAffinity so Custom Tabs appear as a separate task from the main app.
|
* Uses an empty taskAffinity so Custom Tabs appear as a separate task from the main app.
|
||||||
*/
|
*/
|
||||||
class ExternalAppBrowserActivity : AppCompatActivity() {
|
open class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ExternalAppBrowserActivity"
|
private const val TAG = "ExternalAppBrowserActivity"
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -47,6 +47,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSessionApi
|
|||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncApi
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
|
||||||
@@ -155,7 +157,9 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
profileFolder: String,
|
profileFolder: String,
|
||||||
logLevel: LogLevel,
|
logLevel: LogLevel,
|
||||||
contentBlocking: ContentBlocking,
|
contentBlocking: ContentBlocking,
|
||||||
addonCollection: AddonCollection?
|
addonCollection: AddonCollection?,
|
||||||
|
fxaServerOverride: String?,
|
||||||
|
syncTokenServerOverride: String?,
|
||||||
) {
|
) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (!isGeckoInitialized) {
|
if (!isGeckoInitialized) {
|
||||||
@@ -170,7 +174,14 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
|
|
||||||
Log.addSink(PriorityAwareLogSink(level, geckoLogging))
|
Log.addSink(PriorityAwareLogSink(level, geckoLogging))
|
||||||
|
|
||||||
setupGeckoEngine(profileFolder, level, contentBlocking, addonCollection)
|
setupGeckoEngine(
|
||||||
|
profileFolder,
|
||||||
|
level,
|
||||||
|
contentBlocking,
|
||||||
|
addonCollection,
|
||||||
|
fxaServerOverride,
|
||||||
|
syncTokenServerOverride,
|
||||||
|
)
|
||||||
isGeckoInitialized = true
|
isGeckoInitialized = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +201,9 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
profileFolder: String,
|
profileFolder: String,
|
||||||
logLevel: Log.Priority,
|
logLevel: Log.Priority,
|
||||||
contentBlocking: ContentBlocking,
|
contentBlocking: ContentBlocking,
|
||||||
addonCollection: AddonCollection?
|
addonCollection: AddonCollection?,
|
||||||
|
fxaServerOverride: String?,
|
||||||
|
syncTokenServerOverride: String?,
|
||||||
) {
|
) {
|
||||||
val profileApplicationContext = ProfileContext(_flutterPluginBinding.applicationContext, profileFolder)
|
val profileApplicationContext = ProfileContext(_flutterPluginBinding.applicationContext, profileFolder)
|
||||||
|
|
||||||
@@ -220,6 +233,8 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
GeckoSuggestionApiImpl(suggestionEvents)
|
GeckoSuggestionApiImpl(suggestionEvents)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val syncStateEvents = GeckoSyncStateEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
GlobalComponents.setUp(
|
GlobalComponents.setUp(
|
||||||
profileApplicationContext,
|
profileApplicationContext,
|
||||||
_flutterEvents,
|
_flutterEvents,
|
||||||
@@ -228,9 +243,12 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
addonEvents,
|
addonEvents,
|
||||||
tabContentEvents,
|
tabContentEvents,
|
||||||
extensionEvents,
|
extensionEvents,
|
||||||
|
syncStateEvents,
|
||||||
logLevel,
|
logLevel,
|
||||||
contentBlocking,
|
contentBlocking,
|
||||||
addonCollection
|
addonCollection,
|
||||||
|
fxaServerOverride,
|
||||||
|
syncTokenServerOverride,
|
||||||
)
|
)
|
||||||
|
|
||||||
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl()
|
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl()
|
||||||
@@ -275,6 +293,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext))
|
GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext))
|
||||||
GeckoTrackingProtectionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTrackingProtectionApiImpl())
|
GeckoTrackingProtectionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTrackingProtectionApiImpl())
|
||||||
GeckoAppLinksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAppLinksApiImpl(profileApplicationContext))
|
GeckoAppLinksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAppLinksApiImpl(profileApplicationContext))
|
||||||
|
GeckoSyncApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSyncApiImpl())
|
||||||
|
|
||||||
// PWA API for web app installation and management
|
// PWA API for web app installation and management
|
||||||
GeckoPwaApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPwaApiImpl(profileApplicationContext))
|
GeckoPwaApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPwaApiImpl(profileApplicationContext))
|
||||||
|
|||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.api
|
||||||
|
|
||||||
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.components.WebLibreFxAEntryPoint
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncApi
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncAccountInfo
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncDevice
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncDeviceTabs
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineStatus
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineValue
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncIncomingTab
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncRemoteTab
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.concept.sync.DeviceCapability
|
||||||
|
import mozilla.components.concept.sync.DeviceCommandOutgoing
|
||||||
|
import mozilla.components.concept.sync.TabData
|
||||||
|
import mozilla.components.service.fxa.SyncEngine
|
||||||
|
import mozilla.components.service.fxa.manager.SCOPE_PROFILE
|
||||||
|
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
||||||
|
import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||||
|
import mozilla.components.service.fxa.sync.SyncReason
|
||||||
|
import mozilla.components.service.fxa.sync.getLastSynced
|
||||||
|
|
||||||
|
class GeckoSyncApiImpl : GeckoSyncApi {
|
||||||
|
companion object {
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val components by lazy {
|
||||||
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccountInfo(callback: (Result<SyncAccountInfo>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val accountManager = components.backgroundServices.accountManager
|
||||||
|
val account = accountManager.authenticatedAccount()
|
||||||
|
val needsReauth = accountManager.accountNeedsReauth()
|
||||||
|
val profile = account?.getProfile()
|
||||||
|
val engineStorage = SyncEnginesStorage(components.profileApplicationContext)
|
||||||
|
val engineStatus = engineStorage.getStatus()
|
||||||
|
|
||||||
|
callback(
|
||||||
|
Result.success(
|
||||||
|
SyncAccountInfo(
|
||||||
|
authenticated = account != null && !needsReauth,
|
||||||
|
syncing = accountManager.isSyncActive(),
|
||||||
|
needsReauth = needsReauth,
|
||||||
|
email = profile?.email,
|
||||||
|
displayName = profile?.displayName,
|
||||||
|
lastSyncedAt = getLastSynced(components.profileApplicationContext)
|
||||||
|
.takeIf { it > 0L },
|
||||||
|
engines = listOf(
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.HISTORY,
|
||||||
|
enabled = engineStatus[SyncEngine.History] ?: true,
|
||||||
|
),
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.BOOKMARKS,
|
||||||
|
enabled = engineStatus[SyncEngine.Bookmarks] ?: true,
|
||||||
|
),
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.TABS,
|
||||||
|
enabled = engineStatus[SyncEngine.Tabs] ?: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beginAuthentication(callback: (Result<Unit>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
components.services.accountsAuthFeature.beginAuthentication(
|
||||||
|
context = components.profileApplicationContext,
|
||||||
|
entrypoint = WebLibreFxAEntryPoint.Settings,
|
||||||
|
scopes = setOf(SCOPE_PROFILE, SCOPE_SYNC),
|
||||||
|
)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beginPairingAuthentication(
|
||||||
|
pairingUrl: String,
|
||||||
|
callback: (Result<Unit>) -> Unit,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
components.services.accountsAuthFeature.beginPairingAuthentication(
|
||||||
|
context = components.profileApplicationContext,
|
||||||
|
pairingUrl = pairingUrl,
|
||||||
|
entrypoint = WebLibreFxAEntryPoint.Settings,
|
||||||
|
scopes = setOf(SCOPE_PROFILE, SCOPE_SYNC),
|
||||||
|
)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout(callback: (Result<Unit>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.accountManager.logout()
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun syncNow(callback: (Result<Unit>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.accountManager.syncNow(SyncReason.User)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEngineEnabled(
|
||||||
|
engine: SyncEngineValue,
|
||||||
|
enabled: Boolean,
|
||||||
|
callback: (Result<Unit>) -> Unit,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
val storage = SyncEnginesStorage(components.profileApplicationContext)
|
||||||
|
val mapped = when (engine) {
|
||||||
|
SyncEngineValue.HISTORY -> SyncEngine.History
|
||||||
|
SyncEngineValue.BOOKMARKS -> SyncEngine.Bookmarks
|
||||||
|
SyncEngineValue.TABS -> SyncEngine.Tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setStatus(mapped, enabled)
|
||||||
|
components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSyncedTabs(callback: (Result<List<SyncDeviceTabs>>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
val deviceNames = loadDeviceDisplayNames()
|
||||||
|
components.core.remoteTabsStorage.getAll().entries.mapNotNull { (client, tabs) ->
|
||||||
|
val deviceName = deviceNames[client.id] ?: return@mapNotNull null
|
||||||
|
SyncDeviceTabs(
|
||||||
|
deviceId = client.id,
|
||||||
|
deviceName = deviceName,
|
||||||
|
tabs = tabs.map { tab ->
|
||||||
|
val active = tab.active()
|
||||||
|
SyncRemoteTab(
|
||||||
|
title = active.title,
|
||||||
|
url = active.url,
|
||||||
|
iconUrl = active.iconUrl,
|
||||||
|
lastUsed = tab.lastUsed,
|
||||||
|
inactive = tab.inactive,
|
||||||
|
)
|
||||||
|
}.sortedByDescending { it.lastUsed },
|
||||||
|
)
|
||||||
|
}.sortedBy { it.deviceName.lowercase() }
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDevices(callback: (Result<List<SyncDevice>>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching emptyList()
|
||||||
|
|
||||||
|
val constellation = account.deviceConstellation()
|
||||||
|
constellation.refreshDevices()
|
||||||
|
val state = constellation.state()
|
||||||
|
val currentDevice = state?.currentDevice
|
||||||
|
val otherDevices = state?.otherDevices.orEmpty()
|
||||||
|
|
||||||
|
(listOfNotNull(currentDevice) + otherDevices).map { device ->
|
||||||
|
SyncDevice(
|
||||||
|
deviceId = device.id,
|
||||||
|
displayName = device.displayName,
|
||||||
|
isCurrentDevice = device.isCurrentDevice,
|
||||||
|
canSendTab = device.capabilities.contains(DeviceCapability.SEND_TAB),
|
||||||
|
)
|
||||||
|
}.sortedBy { it.displayName.lowercase() }
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sendTabToDevice(
|
||||||
|
deviceId: String,
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
callback: (Result<Boolean>) -> Unit,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching false
|
||||||
|
|
||||||
|
val constellation = account.deviceConstellation()
|
||||||
|
constellation.refreshDevices()
|
||||||
|
val state = constellation.state()
|
||||||
|
val target = state?.otherDevices?.firstOrNull {
|
||||||
|
it.id == deviceId && it.capabilities.contains(DeviceCapability.SEND_TAB)
|
||||||
|
} ?: return@runCatching false
|
||||||
|
|
||||||
|
constellation.sendCommandToDevice(
|
||||||
|
target.id,
|
||||||
|
DeviceCommandOutgoing.SendTab(
|
||||||
|
title = title,
|
||||||
|
url = url,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refreshDevices(callback: (Result<Unit>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching
|
||||||
|
|
||||||
|
account.deviceConstellation().refreshDevices()
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pollDeviceCommands(callback: (Result<Unit>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching
|
||||||
|
|
||||||
|
account.deviceConstellation().pollForCommands()
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(Unit)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drainIncomingTabs(callback: (Result<List<SyncIncomingTab>>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.drainIncomingTabs().map {
|
||||||
|
SyncIncomingTab(
|
||||||
|
title = it.title,
|
||||||
|
url = it.url,
|
||||||
|
fromDeviceId = it.fromDeviceId,
|
||||||
|
fromDeviceName = it.fromDeviceName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDeviceName(callback: (Result<String?>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching null
|
||||||
|
|
||||||
|
val constellation = account.deviceConstellation()
|
||||||
|
constellation.refreshDevices()
|
||||||
|
constellation.state()?.currentDevice?.displayName
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDeviceName(newName: String, callback: (Result<Boolean>) -> Unit) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
val trimmed = newName.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return@runCatching false
|
||||||
|
}
|
||||||
|
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return@runCatching false
|
||||||
|
|
||||||
|
account.deviceConstellation()
|
||||||
|
.setDeviceName(trimmed, components.profileApplicationContext)
|
||||||
|
}.fold(
|
||||||
|
onSuccess = { callback(Result.success(it)) },
|
||||||
|
onFailure = { callback(Result.failure(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadDeviceDisplayNames(): Map<String, String> {
|
||||||
|
components.backgroundServices.awaitStarted()
|
||||||
|
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||||
|
?: return emptyMap()
|
||||||
|
|
||||||
|
val constellation = account.deviceConstellation()
|
||||||
|
constellation.refreshDevices()
|
||||||
|
val state = constellation.state() ?: return emptyMap()
|
||||||
|
val devices = listOfNotNull(state.currentDevice) + state.otherDevices
|
||||||
|
return devices.associate { it.id to it.displayName }
|
||||||
|
}
|
||||||
|
}
|
||||||
+315
@@ -0,0 +1,315 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.os.Build
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
|
import mozilla.components.concept.sync.AuthType
|
||||||
|
import mozilla.components.concept.sync.Device
|
||||||
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
|
import mozilla.components.concept.sync.Profile
|
||||||
|
import mozilla.components.concept.sync.TabData
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
|
import mozilla.components.browser.storage.sync.RemoteTabsStorage
|
||||||
|
import mozilla.components.concept.sync.DeviceConfig
|
||||||
|
import mozilla.components.concept.sync.DeviceCapability
|
||||||
|
import mozilla.components.concept.sync.DeviceType
|
||||||
|
import mozilla.components.feature.accounts.push.SendTabFeature
|
||||||
|
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
|
||||||
|
import mozilla.components.service.fxa.PeriodicSyncConfig
|
||||||
|
import mozilla.components.service.fxa.ServerConfig
|
||||||
|
import mozilla.components.service.fxa.SyncConfig
|
||||||
|
import mozilla.components.service.fxa.SyncEngine
|
||||||
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
|
import mozilla.components.service.fxa.manager.SCOPE_SESSION
|
||||||
|
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
||||||
|
import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||||
|
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
||||||
|
import mozilla.components.service.fxa.sync.SyncReason
|
||||||
|
import mozilla.components.service.fxa.sync.SyncStatusObserver
|
||||||
|
import mozilla.components.service.fxa.sync.getLastSynced
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncAccountInfo
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineStatus
|
||||||
|
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineValue
|
||||||
|
import eu.weblibre.flutter_mozilla_components.sync.SyncedTabsIntegration
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class BackgroundServices(
|
||||||
|
private val context: Context,
|
||||||
|
private val browserStore: Lazy<BrowserStore>,
|
||||||
|
private val historyStorage: Lazy<PlacesHistoryStorage>,
|
||||||
|
private val bookmarkStorage: Lazy<PlacesBookmarksStorage>,
|
||||||
|
private val remoteTabsStorage: Lazy<RemoteTabsStorage>,
|
||||||
|
private val fxaServerOverride: String?,
|
||||||
|
private val syncTokenServerOverride: String?,
|
||||||
|
private val syncStateEvents: GeckoSyncStateEvents?,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val MIN_STARTUP_SYNC_INTERVAL_MS = 15 * 60 * 1000L
|
||||||
|
private val MAX_ACTIVE_TIME_MS = TimeUnit.DAYS.toMillis(14L)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class IncomingTab(
|
||||||
|
val title: String,
|
||||||
|
val url: String,
|
||||||
|
val fromDeviceId: String?,
|
||||||
|
val fromDeviceName: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val incomingTabsLock = Any()
|
||||||
|
private val incomingTabsQueue = ArrayDeque<IncomingTab>()
|
||||||
|
private val startedSignal = CompletableDeferred<Unit>()
|
||||||
|
private val authStateScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
private val authStateLock = Any()
|
||||||
|
private var lastAuthState: SyncAccountInfo? = null
|
||||||
|
private val isDebuggable =
|
||||||
|
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
|
|
||||||
|
private val supportedEngines = setOf(
|
||||||
|
SyncEngine.History,
|
||||||
|
SyncEngine.Bookmarks,
|
||||||
|
SyncEngine.Tabs,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val syncConfig = SyncConfig(
|
||||||
|
supportedEngines,
|
||||||
|
periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 240),
|
||||||
|
)
|
||||||
|
|
||||||
|
val syncedTabsStorage by lazy {
|
||||||
|
SyncedTabsStorage(
|
||||||
|
accountManager,
|
||||||
|
browserStore.value,
|
||||||
|
remoteTabsStorage.value,
|
||||||
|
MAX_ACTIVE_TIME_MS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverConfig: ServerConfig = FxaServer.config(
|
||||||
|
context = context,
|
||||||
|
serverOverride = fxaServerOverride,
|
||||||
|
tokenServerOverride = syncTokenServerOverride,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val deviceConfig = DeviceConfig(
|
||||||
|
name = "WebLibre ${Build.MANUFACTURER} ${Build.MODEL}",
|
||||||
|
type = DeviceType.MOBILE,
|
||||||
|
capabilities = setOf(DeviceCapability.SEND_TAB, DeviceCapability.CLOSE_TABS),
|
||||||
|
secureStateAtRest = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
||||||
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
|
||||||
|
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sequenceCounter = AtomicLong(0)
|
||||||
|
private val syncedTabsIntegrationLaunched = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private fun dispatchAuthState(account: OAuthAccount?, needsReauth: Boolean = false) {
|
||||||
|
val events = syncStateEvents ?: return
|
||||||
|
authStateScope.launch {
|
||||||
|
val profile = account?.getProfile()
|
||||||
|
val engineStorage = SyncEnginesStorage(context)
|
||||||
|
val engineStatus = engineStorage.getStatus()
|
||||||
|
|
||||||
|
val info = SyncAccountInfo(
|
||||||
|
authenticated = account != null && !needsReauth,
|
||||||
|
syncing = accountManager.isSyncActive(),
|
||||||
|
needsReauth = needsReauth,
|
||||||
|
email = profile?.email,
|
||||||
|
displayName = profile?.displayName,
|
||||||
|
lastSyncedAt = getLastSynced(context).takeIf { it > 0L },
|
||||||
|
engines = listOf(
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.HISTORY,
|
||||||
|
enabled = engineStatus[SyncEngine.History] ?: true,
|
||||||
|
),
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.BOOKMARKS,
|
||||||
|
enabled = engineStatus[SyncEngine.Bookmarks] ?: true,
|
||||||
|
),
|
||||||
|
SyncEngineStatus(
|
||||||
|
engine = SyncEngineValue.TABS,
|
||||||
|
enabled = engineStatus[SyncEngine.Tabs] ?: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val shouldEmit = synchronized(authStateLock) {
|
||||||
|
if (lastAuthState == info) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
lastAuthState = info
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldEmit) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
events.onAuthStateChanged(sequenceCounter.incrementAndGet(), info) { _ -> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountManager: FxaAccountManager by lazy {
|
||||||
|
FxaAccountManager(
|
||||||
|
context = context,
|
||||||
|
serverConfig = serverConfig,
|
||||||
|
deviceConfig = deviceConfig,
|
||||||
|
syncConfig = syncConfig,
|
||||||
|
applicationScopes = setOf(SCOPE_SYNC, SCOPE_SESSION),
|
||||||
|
crashReporter = null,
|
||||||
|
).also { accountManager ->
|
||||||
|
SendTabFeature(accountManager) { device: Device?, tabs: List<TabData> ->
|
||||||
|
synchronized(incomingTabsLock) {
|
||||||
|
tabs.forEach { tab ->
|
||||||
|
incomingTabsQueue.addLast(
|
||||||
|
IncomingTab(
|
||||||
|
title = tab.title,
|
||||||
|
url = tab.url,
|
||||||
|
fromDeviceId = device?.id,
|
||||||
|
fromDeviceName = device?.displayName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.register(object : AccountObserver {
|
||||||
|
override fun onReady(authenticatedAccount: OAuthAccount?) {
|
||||||
|
if (!startedSignal.isCompleted) {
|
||||||
|
startedSignal.complete(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||||
|
dispatchAuthState(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationProblems() {
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount(), needsReauth = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoggedOut() {
|
||||||
|
dispatchAuthState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProfileUpdated(profile: Profile) {
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFlowError(error: mozilla.components.concept.sync.AuthFlowError) {
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount(), needsReauth = true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
accountManager.registerForSyncEvents(object : SyncStatusObserver {
|
||||||
|
override fun onStarted() {
|
||||||
|
val events = syncStateEvents ?: return
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount())
|
||||||
|
runOnUiThread {
|
||||||
|
events.onSyncStarted(sequenceCounter.incrementAndGet()) { _ -> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIdle() {
|
||||||
|
val events = syncStateEvents ?: return
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount())
|
||||||
|
runOnUiThread {
|
||||||
|
events.onSyncCompleted(sequenceCounter.incrementAndGet()) { _ -> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(error: Exception?) {
|
||||||
|
val events = syncStateEvents ?: return
|
||||||
|
dispatchAuthState(accountManager.authenticatedAccount())
|
||||||
|
runOnUiThread {
|
||||||
|
events.onSyncError(
|
||||||
|
sequenceCounter.incrementAndGet(),
|
||||||
|
error?.message,
|
||||||
|
) { _ -> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, owner = ProcessLifecycleOwner.get(), autoPause = false)
|
||||||
|
|
||||||
|
MainScope().launch {
|
||||||
|
runCatching {
|
||||||
|
accountManager.start()
|
||||||
|
}.fold(
|
||||||
|
onSuccess = {},
|
||||||
|
onFailure = { error ->
|
||||||
|
val isDuplicateInitialize = error.message
|
||||||
|
?.contains("Initialize already sent", ignoreCase = true)
|
||||||
|
?: false
|
||||||
|
|
||||||
|
if (isDuplicateInitialize) {
|
||||||
|
return@fold
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startedSignal.isCompleted) {
|
||||||
|
startedSignal.completeExceptionally(error)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (accountManager.authenticatedAccount() != null && shouldSyncOnStartup()) {
|
||||||
|
accountManager.syncNow(SyncReason.Startup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun awaitStarted() {
|
||||||
|
accountManager
|
||||||
|
launchSyncedTabsIntegrationIfNeeded()
|
||||||
|
withTimeout(10_000) {
|
||||||
|
startedSignal.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSyncedTabsIntegrationIfNeeded() {
|
||||||
|
if (syncedTabsIntegrationLaunched.compareAndSet(false, true)) {
|
||||||
|
SyncedTabsIntegration(accountManager, syncedTabsStorage).launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldSyncOnStartup(): Boolean {
|
||||||
|
val lastSynced = getLastSynced(context)
|
||||||
|
if (lastSynced <= 0L) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (System.currentTimeMillis() - lastSynced) >= MIN_STARTUP_SYNC_INTERVAL_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drainIncomingTabs(): List<IncomingTab> {
|
||||||
|
synchronized(incomingTabsLock) {
|
||||||
|
if (incomingTabsQueue.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val values = incomingTabsQueue.toList()
|
||||||
|
incomingTabsQueue.clear()
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -33,6 +33,7 @@ import mozilla.components.browser.state.engine.middleware.SessionPrioritizationM
|
|||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||||
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
|
import mozilla.components.browser.storage.sync.RemoteTabsStorage
|
||||||
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||||
import mozilla.components.concept.engine.DefaultSettings
|
import mozilla.components.concept.engine.DefaultSettings
|
||||||
@@ -62,8 +63,11 @@ import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
|||||||
import mozilla.components.feature.session.middleware.undo.UndoMiddleware
|
import mozilla.components.feature.session.middleware.undo.UndoMiddleware
|
||||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||||
|
import mozilla.components.concept.base.crash.Breadcrumb
|
||||||
|
import mozilla.components.concept.base.crash.CrashReporting
|
||||||
import mozilla.components.support.base.worker.Frequency
|
import mozilla.components.support.base.worker.Frequency
|
||||||
import org.mozilla.geckoview.GeckoRuntime
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val AMO_COLLECTION_MAX_CACHE_AGE = 24 * 60L
|
private const val AMO_COLLECTION_MAX_CACHE_AGE = 24 * 60L
|
||||||
@@ -74,6 +78,12 @@ class Core(
|
|||||||
private val flutterEvents: GeckoStateEvents,
|
private val flutterEvents: GeckoStateEvents,
|
||||||
private val extensionEvents: BrowserExtensionEvents
|
private val extensionEvents: BrowserExtensionEvents
|
||||||
) {
|
) {
|
||||||
|
private val noOpCrashReporter = object : CrashReporting {
|
||||||
|
override fun submitCaughtException(throwable: Throwable): Job = Job()
|
||||||
|
|
||||||
|
override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
val prefs by lazy {
|
val prefs by lazy {
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
}
|
}
|
||||||
@@ -251,12 +261,14 @@ class Core(
|
|||||||
*/
|
*/
|
||||||
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
|
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
|
||||||
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) }
|
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) }
|
||||||
|
val lazyRemoteTabsStorage = lazy { RemoteTabsStorage(context, noOpCrashReporter) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A convenience accessor to the [PlacesHistoryStorage].
|
* A convenience accessor to the [PlacesHistoryStorage].
|
||||||
*/
|
*/
|
||||||
val historyStorage by lazy { lazyHistoryStorage.value }
|
val historyStorage by lazy { lazyHistoryStorage.value }
|
||||||
val bookmarksStorage by lazy { lazyBookmarksStorage.value }
|
val bookmarksStorage by lazy { lazyBookmarksStorage.value }
|
||||||
|
val remoteTabsStorage by lazy { lazyRemoteTabsStorage.value }
|
||||||
|
|
||||||
val permissionStorage by lazy { PermissionStorage(geckoSitePermissionsStorage) }
|
val permissionStorage by lazy { PermissionStorage(geckoSitePermissionsStorage) }
|
||||||
|
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.appservices.fxaclient.FxaServer as AppServicesFxaServer
|
||||||
|
import mozilla.components.service.fxa.ServerConfig
|
||||||
|
|
||||||
|
object FxaServer {
|
||||||
|
private const val CLIENT_ID = "a2270f727f45f648"
|
||||||
|
const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
|
||||||
|
|
||||||
|
fun config(
|
||||||
|
context: Context,
|
||||||
|
serverOverride: String?,
|
||||||
|
tokenServerOverride: String?
|
||||||
|
): ServerConfig {
|
||||||
|
val effectiveServerOverride = serverOverride?.trim().orEmpty()
|
||||||
|
val effectiveTokenOverride = tokenServerOverride?.trim().takeUnless { it.isNullOrEmpty() }
|
||||||
|
|
||||||
|
return if (effectiveServerOverride.isEmpty()) {
|
||||||
|
ServerConfig(AppServicesFxaServer.Release, CLIENT_ID, REDIRECT_URL, effectiveTokenOverride)
|
||||||
|
} else {
|
||||||
|
ServerConfig(
|
||||||
|
AppServicesFxaServer.Custom(effectiveServerOverride),
|
||||||
|
CLIENT_ID,
|
||||||
|
REDIRECT_URL,
|
||||||
|
effectiveTokenOverride,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -5,13 +5,26 @@
|
|||||||
package eu.weblibre.flutter_mozilla_components.components
|
package eu.weblibre.flutter_mozilla_components.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
import eu.weblibre.flutter_mozilla_components.R
|
import eu.weblibre.flutter_mozilla_components.R
|
||||||
|
import eu.weblibre.flutter_mozilla_components.activities.AuthIntentReceiverActivity
|
||||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.Engine
|
||||||
|
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||||
|
import mozilla.components.feature.accounts.FxaCapability
|
||||||
|
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
||||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
|
import mozilla.components.service.fxa.ServerConfig
|
||||||
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component group which encapsulates foreground-friendly services.
|
* Component group which encapsulates foreground-friendly services.
|
||||||
@@ -20,9 +33,39 @@ class Services(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val store: BrowserStore,
|
private val store: BrowserStore,
|
||||||
private val tabsUseCases: TabsUseCases,
|
private val tabsUseCases: TabsUseCases,
|
||||||
|
accountManager: FxaAccountManager,
|
||||||
|
private val engine: Engine,
|
||||||
|
private val serverConfig: ServerConfig,
|
||||||
) {
|
) {
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val accountsAuthFeature by lazy {
|
||||||
|
FirefoxAccountsAuthFeature(accountManager, FxaServer.REDIRECT_URL) { _, authUrl ->
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
val intent = CustomTabsIntent.Builder()
|
||||||
|
.setInstantAppsEnabled(false)
|
||||||
|
.build()
|
||||||
|
.intent
|
||||||
|
.setData(authUrl.toUri())
|
||||||
|
.setClassName(context, AuthIntentReceiverActivity::class.java.name)
|
||||||
|
.setPackage(context.packageName)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fxaWebChannelFeature by lazy {
|
||||||
|
FxaWebChannelFeature(
|
||||||
|
customTabSessionId = null,
|
||||||
|
runtime = engine,
|
||||||
|
store = store,
|
||||||
|
accountManager = accountManager,
|
||||||
|
serverConfig = serverConfig,
|
||||||
|
fxaCapabilities = setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val appLinksInterceptor by lazy {
|
val appLinksInterceptor by lazy {
|
||||||
AppLinksInterceptor(
|
AppLinksInterceptor(
|
||||||
context = context,
|
context = context,
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.components
|
||||||
|
|
||||||
|
import mozilla.components.concept.sync.FxAEntryPoint
|
||||||
|
|
||||||
|
enum class WebLibreFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
|
||||||
|
Settings("settings"),
|
||||||
|
}
|
||||||
+14
-1
@@ -42,6 +42,19 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
components.services.accountsAuthFeature.interceptor.onLoadRequest(
|
||||||
|
engineSession,
|
||||||
|
uri,
|
||||||
|
lastUri,
|
||||||
|
hasUserGesture,
|
||||||
|
isSameDomain,
|
||||||
|
isRedirect,
|
||||||
|
isDirectNavigation,
|
||||||
|
isSubframeRequest,
|
||||||
|
)?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
return components.services.appLinksInterceptor.onLoadRequest(
|
return components.services.appLinksInterceptor.onLoadRequest(
|
||||||
engineSession,
|
engineSession,
|
||||||
uri,
|
uri,
|
||||||
@@ -64,4 +77,4 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun interceptsAppInitiatedRequests() = true
|
override fun interceptsAppInitiatedRequests() = true
|
||||||
}
|
}
|
||||||
|
|||||||
+805
-141
File diff suppressed because it is too large
Load Diff
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package eu.weblibre.flutter_mozilla_components.sync
|
||||||
|
|
||||||
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
|
import mozilla.components.concept.sync.AuthType
|
||||||
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
|
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
|
||||||
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts and stops SyncedTabsStorage based on the authentication state.
|
||||||
|
*/
|
||||||
|
class SyncedTabsIntegration(
|
||||||
|
private val accountManager: FxaAccountManager,
|
||||||
|
private val syncedTabsStorage: SyncedTabsStorage,
|
||||||
|
) {
|
||||||
|
fun launch() {
|
||||||
|
accountManager.register(SyncedTabsAccountObserver(syncedTabsStorage))
|
||||||
|
|
||||||
|
if (accountManager.authenticatedAccount() != null) {
|
||||||
|
syncedTabsStorage.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SyncedTabsAccountObserver(
|
||||||
|
private val syncedTabsStorage: SyncedTabsStorage,
|
||||||
|
) : AccountObserver {
|
||||||
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||||
|
syncedTabsStorage.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoggedOut() {
|
||||||
|
syncedTabsStorage.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export 'src/domain/services/gecko_readerable.dart';
|
|||||||
export 'src/domain/services/gecko_selection_action.dart';
|
export 'src/domain/services/gecko_selection_action.dart';
|
||||||
export 'src/domain/services/gecko_session.dart';
|
export 'src/domain/services/gecko_session.dart';
|
||||||
export 'src/domain/services/gecko_suggestions.dart';
|
export 'src/domain/services/gecko_suggestions.dart';
|
||||||
|
export 'src/domain/services/gecko_sync.dart';
|
||||||
export 'src/domain/services/gecko_tab.dart';
|
export 'src/domain/services/gecko_tab.dart';
|
||||||
export 'src/domain/services/gecko_tab_content.dart';
|
export 'src/domain/services/gecko_tab_content.dart';
|
||||||
export 'src/domain/services/gecko_viewport.dart';
|
export 'src/domain/services/gecko_viewport.dart';
|
||||||
@@ -84,6 +85,13 @@ export 'src/pigeons/gecko.g.dart'
|
|||||||
SecurityInfoState,
|
SecurityInfoState,
|
||||||
SitePermissionStatus,
|
SitePermissionStatus,
|
||||||
SitePermissions,
|
SitePermissions,
|
||||||
|
SyncAccountInfo,
|
||||||
|
SyncDevice,
|
||||||
|
SyncDeviceTabs,
|
||||||
|
SyncEngineStatus,
|
||||||
|
SyncEngineValue,
|
||||||
|
SyncIncomingTab,
|
||||||
|
SyncRemoteTab,
|
||||||
TabContent,
|
TabContent,
|
||||||
TabContentState,
|
TabContentState,
|
||||||
TrackingProtectionException,
|
TrackingProtectionException,
|
||||||
|
|||||||
@@ -22,12 +22,16 @@ class GeckoBrowserService {
|
|||||||
LogLevel logLevel,
|
LogLevel logLevel,
|
||||||
ContentBlocking contentBlocking,
|
ContentBlocking contentBlocking,
|
||||||
AddonCollection? addonCollection,
|
AddonCollection? addonCollection,
|
||||||
|
String? fxaServerOverride,
|
||||||
|
String? syncTokenServerOverride,
|
||||||
) {
|
) {
|
||||||
return _api.initialize(
|
return _api.initialize(
|
||||||
profileFolder,
|
profileFolder,
|
||||||
logLevel,
|
logLevel,
|
||||||
contentBlocking,
|
contentBlocking,
|
||||||
addonCollection,
|
addonCollection,
|
||||||
|
fxaServerOverride,
|
||||||
|
syncTokenServerOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
|
||||||
|
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
final _apiInstance = GeckoSyncApi();
|
||||||
|
|
||||||
|
class GeckoSyncService {
|
||||||
|
final GeckoSyncApi _api;
|
||||||
|
|
||||||
|
GeckoSyncService({GeckoSyncApi? api}) : _api = api ?? _apiInstance;
|
||||||
|
|
||||||
|
Future<SyncAccountInfo> getAccountInfo() {
|
||||||
|
return _api.getAccountInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> beginAuthentication() {
|
||||||
|
return _api.beginAuthentication();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> beginPairingAuthentication(String pairingUrl) {
|
||||||
|
return _api.beginPairingAuthentication(pairingUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() {
|
||||||
|
return _api.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> syncNow() {
|
||||||
|
return _api.syncNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setEngineEnabled(SyncEngineValue engine, bool enabled) {
|
||||||
|
return _api.setEngineEnabled(engine, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SyncDeviceTabs>> getSyncedTabs() {
|
||||||
|
return _api.getSyncedTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SyncDevice>> getDevices() {
|
||||||
|
return _api.getDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> sendTabToDevice(String deviceId, String title, String url) {
|
||||||
|
return _api.sendTabToDevice(deviceId, title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshDevices() {
|
||||||
|
return _api.refreshDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pollDeviceCommands() {
|
||||||
|
return _api.pollDeviceCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SyncIncomingTab>> drainIncomingTabs() {
|
||||||
|
return _api.drainIncomingTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getDeviceName() {
|
||||||
|
return _api.getDeviceName();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> setDeviceName(String newName) {
|
||||||
|
return _api.setDeviceName(newName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeckoSyncStateService extends GeckoSyncStateEvents {
|
||||||
|
final _authStateSubject = BehaviorSubject<SyncAccountInfo>();
|
||||||
|
final _syncStartedSubject = PublishSubject<void>();
|
||||||
|
final _syncCompletedSubject = PublishSubject<void>();
|
||||||
|
final _syncErrorSubject = PublishSubject<String?>();
|
||||||
|
|
||||||
|
ValueStream<SyncAccountInfo> get authStateEvents => _authStateSubject.stream;
|
||||||
|
Stream<void> get syncStartedEvents => _syncStartedSubject.stream;
|
||||||
|
Stream<void> get syncCompletedEvents => _syncCompletedSubject.stream;
|
||||||
|
Stream<String?> get syncErrorEvents => _syncErrorSubject.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onAuthStateChanged(int sequence, SyncAccountInfo accountInfo) {
|
||||||
|
_authStateSubject.addWhenMoreRecent(sequence, null, accountInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSyncStarted(int sequence) {
|
||||||
|
_syncStartedSubject.addWhenMoreRecent(sequence, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSyncCompleted(int sequence) {
|
||||||
|
_syncCompletedSubject.addWhenMoreRecent(sequence, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSyncError(int sequence, String? errorMessage) {
|
||||||
|
_syncErrorSubject.addWhenMoreRecent(sequence, null, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
GeckoSyncStateService.setUp({
|
||||||
|
BinaryMessenger? binaryMessenger,
|
||||||
|
String messageChannelSuffix = '',
|
||||||
|
}) {
|
||||||
|
GeckoSyncStateEvents.setUp(
|
||||||
|
this,
|
||||||
|
binaryMessenger: binaryMessenger,
|
||||||
|
messageChannelSuffix: messageChannelSuffix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await Future.wait([
|
||||||
|
_authStateSubject.close(),
|
||||||
|
_syncStartedSubject.close(),
|
||||||
|
_syncCompletedSubject.close(),
|
||||||
|
_syncErrorSubject.close(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -998,11 +998,143 @@ abstract class GeckoBrowserApi {
|
|||||||
LogLevel logLevel,
|
LogLevel logLevel,
|
||||||
ContentBlocking contentBlocking,
|
ContentBlocking contentBlocking,
|
||||||
AddonCollection? addonCollection,
|
AddonCollection? addonCollection,
|
||||||
|
String? fxaServerOverride,
|
||||||
|
String? syncTokenServerOverride,
|
||||||
);
|
);
|
||||||
bool showNativeFragment();
|
bool showNativeFragment();
|
||||||
void onTrimMemory(int level);
|
void onTrimMemory(int level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SyncEngineValue { history, bookmarks, tabs }
|
||||||
|
|
||||||
|
class SyncEngineStatus {
|
||||||
|
final SyncEngineValue engine;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
SyncEngineStatus({required this.engine, required this.enabled});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncAccountInfo {
|
||||||
|
final bool authenticated;
|
||||||
|
final bool syncing;
|
||||||
|
final bool needsReauth;
|
||||||
|
final String? email;
|
||||||
|
final String? displayName;
|
||||||
|
final int? lastSyncedAt;
|
||||||
|
final List<SyncEngineStatus> engines;
|
||||||
|
|
||||||
|
SyncAccountInfo({
|
||||||
|
required this.authenticated,
|
||||||
|
required this.syncing,
|
||||||
|
required this.needsReauth,
|
||||||
|
required this.email,
|
||||||
|
required this.displayName,
|
||||||
|
required this.lastSyncedAt,
|
||||||
|
required this.engines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncDevice {
|
||||||
|
final String deviceId;
|
||||||
|
final String displayName;
|
||||||
|
final bool isCurrentDevice;
|
||||||
|
final bool canSendTab;
|
||||||
|
|
||||||
|
SyncDevice({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.displayName,
|
||||||
|
required this.isCurrentDevice,
|
||||||
|
required this.canSendTab,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncIncomingTab {
|
||||||
|
final String title;
|
||||||
|
final String url;
|
||||||
|
final String? fromDeviceId;
|
||||||
|
final String? fromDeviceName;
|
||||||
|
|
||||||
|
SyncIncomingTab({
|
||||||
|
required this.title,
|
||||||
|
required this.url,
|
||||||
|
required this.fromDeviceId,
|
||||||
|
required this.fromDeviceName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncRemoteTab {
|
||||||
|
final String title;
|
||||||
|
final String url;
|
||||||
|
final String? iconUrl;
|
||||||
|
final int lastUsed;
|
||||||
|
final bool inactive;
|
||||||
|
|
||||||
|
SyncRemoteTab({
|
||||||
|
required this.title,
|
||||||
|
required this.url,
|
||||||
|
required this.iconUrl,
|
||||||
|
required this.lastUsed,
|
||||||
|
required this.inactive,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncDeviceTabs {
|
||||||
|
final String deviceId;
|
||||||
|
final String deviceName;
|
||||||
|
final List<SyncRemoteTab> tabs;
|
||||||
|
|
||||||
|
SyncDeviceTabs({
|
||||||
|
required this.deviceId,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.tabs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostApi()
|
||||||
|
abstract class GeckoSyncApi {
|
||||||
|
@async
|
||||||
|
SyncAccountInfo getAccountInfo();
|
||||||
|
|
||||||
|
@async
|
||||||
|
void beginAuthentication();
|
||||||
|
|
||||||
|
@async
|
||||||
|
void beginPairingAuthentication(String pairingUrl);
|
||||||
|
|
||||||
|
@async
|
||||||
|
void logout();
|
||||||
|
|
||||||
|
@async
|
||||||
|
void syncNow();
|
||||||
|
|
||||||
|
@async
|
||||||
|
void setEngineEnabled(SyncEngineValue engine, bool enabled);
|
||||||
|
|
||||||
|
@async
|
||||||
|
List<SyncDeviceTabs> getSyncedTabs();
|
||||||
|
|
||||||
|
@async
|
||||||
|
List<SyncDevice> getDevices();
|
||||||
|
|
||||||
|
@async
|
||||||
|
bool sendTabToDevice(String deviceId, String title, String url);
|
||||||
|
|
||||||
|
@async
|
||||||
|
void refreshDevices();
|
||||||
|
|
||||||
|
@async
|
||||||
|
void pollDeviceCommands();
|
||||||
|
|
||||||
|
@async
|
||||||
|
List<SyncIncomingTab> drainIncomingTabs();
|
||||||
|
|
||||||
|
@async
|
||||||
|
String? getDeviceName();
|
||||||
|
|
||||||
|
@async
|
||||||
|
bool setDeviceName(String newName);
|
||||||
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class GeckoEngineSettingsApi {
|
abstract class GeckoEngineSettingsApi {
|
||||||
void setDefaultSettings(GeckoEngineSettings settings);
|
void setDefaultSettings(GeckoEngineSettings settings);
|
||||||
@@ -1427,6 +1559,14 @@ abstract class GeckoStateEvents {
|
|||||||
void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest);
|
void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FlutterApi()
|
||||||
|
abstract class GeckoSyncStateEvents {
|
||||||
|
void onAuthStateChanged(int sequence, SyncAccountInfo accountInfo);
|
||||||
|
void onSyncStarted(int sequence);
|
||||||
|
void onSyncCompleted(int sequence);
|
||||||
|
void onSyncError(int sequence, String? errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
@FlutterApi()
|
@FlutterApi()
|
||||||
abstract class GeckoLogging {
|
abstract class GeckoLogging {
|
||||||
void onLog(LogLevel level, String message);
|
void onLog(LogLevel level, String message);
|
||||||
|
|||||||
Reference in New Issue
Block a user