good state
This commit is contained in:
@@ -60,7 +60,7 @@ class TabState extends WebPageInfo {
|
||||
required this.historyState,
|
||||
required this.readerableState,
|
||||
required this.findResultState,
|
||||
}) : super(title: title);
|
||||
}) : super(title: title.trim());
|
||||
|
||||
factory TabState.$default(String tabId) => TabState(
|
||||
id: tabId,
|
||||
|
||||
@@ -22,6 +22,10 @@ class TabSession extends _$TabSession {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> stopLoading() {
|
||||
return _sessionService.stopLoading();
|
||||
}
|
||||
|
||||
Future<void> reload() {
|
||||
return _sessionService.reload();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ final selectedTabSessionNotifierProvider =
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<Raw<TabSession>>;
|
||||
String _$tabSessionHash() => r'e4fbbcd430037cc01adb799783fb7974a11a27bf';
|
||||
String _$tabSessionHash() => r'33d68f3860b3bc9a92db386535c7482bf4416008';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/core/logger.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/states/tab.dart';
|
||||
@@ -14,6 +16,7 @@ import 'package:lensai/features/geckoview/features/browser/domain/providers/inte
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
||||
import 'package:lensai/utils/debouncer.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -80,15 +83,117 @@ class TabRepository extends _$TabRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> selectTab(String tabId) {
|
||||
return _tabsService.selectTab(tabId: tabId);
|
||||
Future<bool> selectTab(String tabId) async {
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.containerTabId(tabId);
|
||||
|
||||
final containerData = await containerId.mapNotNull(
|
||||
(containerId) => ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(containerId),
|
||||
);
|
||||
|
||||
if (containerData != null) {
|
||||
if (containerData.metadata.authSettings.authenticationRequired) {
|
||||
if (containerId != ref.read(selectedContainerProvider)) {
|
||||
logger.w(
|
||||
'Tried to open authenticated tab $tabId but container not selected',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (containerData.metadata.useProxy) {
|
||||
final proxyPluginHealthy =
|
||||
await GeckoContainerProxyService().healthcheck();
|
||||
|
||||
if (!proxyPluginHealthy) {
|
||||
logger.w(
|
||||
'Tried to open proxied tab $tabId but proxy plugin not responding',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _tabsService.selectTab(tabId: tabId);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> closeTab(String tabId) {
|
||||
Future<void> _selectNextTab(String tabId) async {
|
||||
if (ref.read(tabListProvider).value.length == 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentContainerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.containerTabId(tabId);
|
||||
|
||||
final sameContainerTabs = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerTabIds(currentContainerId);
|
||||
|
||||
final nextAvailabeInContainer = sameContainerTabs.firstWhereOrNull(
|
||||
(tab) => tab != tabId,
|
||||
);
|
||||
|
||||
if (nextAvailabeInContainer != null) {
|
||||
return _tabsService.selectTab(tabId: sameContainerTabs.first);
|
||||
}
|
||||
|
||||
final unassignedTabs = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerTabIds(currentContainerId);
|
||||
|
||||
if (unassignedTabs.isNotEmpty) {
|
||||
return _tabsService.selectTab(tabId: unassignedTabs.first);
|
||||
}
|
||||
|
||||
//We only take containers without authentication!
|
||||
final availableContainers =
|
||||
await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getAllContainersWithCount();
|
||||
|
||||
final nextAvailableContainerUnauthenticated = availableContainers
|
||||
.firstWhereOrNull(
|
||||
(container) =>
|
||||
container.metadata.authSettings.authenticationRequired == false,
|
||||
);
|
||||
|
||||
final nextContainerTabs = await nextAvailableContainerUnauthenticated
|
||||
.mapNotNull(
|
||||
(container) => ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerTabIds(container.id),
|
||||
);
|
||||
|
||||
if (nextContainerTabs.isNotEmpty) {
|
||||
return _tabsService.selectTab(tabId: nextContainerTabs!.first);
|
||||
}
|
||||
|
||||
if (availableContainers.isNotEmpty) {
|
||||
//Last resort push new tab to avoid any authenticated tab is selected
|
||||
// ignore: avoid_redundant_argument_values
|
||||
await addTab(selectTab: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTab(String tabId) async {
|
||||
if (ref.read(selectedTabProvider) == tabId) {
|
||||
await _selectNextTab(tabId);
|
||||
}
|
||||
|
||||
return _tabsService.removeTab(tabId: tabId);
|
||||
}
|
||||
|
||||
Future<void> closeTabs(List<String> tabIds) {
|
||||
Future<void> closeTabs(List<String> tabIds) async {
|
||||
final selectedTab = ref.read(selectedTabProvider);
|
||||
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
|
||||
await _selectNextTab(selectedTab!);
|
||||
}
|
||||
|
||||
return _tabsService.removeTabs(ids: tabIds);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabRepositoryHash() => r'35c078a841d815aa2452c07b52c4cf0cbcec7e68';
|
||||
String _$tabRepositoryHash() => r'a798385b50c101e41b9789e92181cbd5b6188246';
|
||||
|
||||
/// See also [TabRepository].
|
||||
@ProviderFor(TabRepository)
|
||||
|
||||
@@ -4,10 +4,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'browser_data.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
@Riverpod(keepAlive: true)
|
||||
class BrowserDataService extends _$BrowserDataService {
|
||||
final _service = GeckoDeleteBrowserDataService();
|
||||
|
||||
var _onStartDeleted = false;
|
||||
|
||||
Future<void> deleteDataOnEngineStart(
|
||||
Set<DeleteBrowsingDataType>? types,
|
||||
) async {
|
||||
if (!_onStartDeleted) {
|
||||
_onStartDeleted = true;
|
||||
return deleteData(types);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteData(Set<DeleteBrowsingDataType>? types) async {
|
||||
if (types != null) {
|
||||
for (final type in types) {
|
||||
|
||||
@@ -7,12 +7,12 @@ part of 'browser_data.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$browserDataServiceHash() =>
|
||||
r'f56c60635505e06dbd5b05f1bcee9c25c4d0eaaf';
|
||||
r'0a00db5b143d3851f2c171f4f939940f959b67bb';
|
||||
|
||||
/// See also [BrowserDataService].
|
||||
@ProviderFor(BrowserDataService)
|
||||
final browserDataServiceProvider =
|
||||
AutoDisposeNotifierProvider<BrowserDataService, void>.internal(
|
||||
NotifierProvider<BrowserDataService, void>.internal(
|
||||
BrowserDataService.new,
|
||||
name: r'browserDataServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -23,6 +23,6 @@ final browserDataServiceProvider =
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$BrowserDataService = AutoDisposeNotifier<void>;
|
||||
typedef _$BrowserDataService = Notifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -61,161 +63,189 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
//We need this for BackButtonListener to work downstream
|
||||
//No direct pop result will be handled here
|
||||
canPop: false,
|
||||
child: Scaffold(
|
||||
bottomNavigationBar: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabInFullScreen = ref.watch(
|
||||
selectedTabStateProvider.select(
|
||||
(value) => value?.isFullScreen ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return Visibility(
|
||||
visible: !tabInFullScreen,
|
||||
child: BrowserBottomAppBar(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
//Remove M3 default of 640p
|
||||
constraints: BoxConstraints(
|
||||
maxWidth:
|
||||
MediaQuery.of(context).size.width -
|
||||
math.max(
|
||||
MediaQuery.of(context).padding.left * 2,
|
||||
MediaQuery.of(context).padding.right * 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: DragTarget<TabDragData>(
|
||||
onMove: (details) {
|
||||
ref
|
||||
.read(willAcceptDropProvider.notifier)
|
||||
.setData(DeleteDropData(details.data.tabId));
|
||||
},
|
||||
onLeave: (data) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
onAcceptWithDetails: (details) async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTab(details.data.tabId);
|
||||
},
|
||||
builder: (context, _, _) {
|
||||
return OverlayPortal(
|
||||
controller: overlayController,
|
||||
overlayChildBuilder: (context) {
|
||||
return displayedOverlay!;
|
||||
},
|
||||
child: Listener(
|
||||
onPointerDown:
|
||||
(displayedSheet != null)
|
||||
? (_) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
}
|
||||
: null,
|
||||
child: BackButtonListener(
|
||||
onBackButtonPressed: () async {
|
||||
final tabState =
|
||||
(selectedTabId != null)
|
||||
? ref.read(tabStateProvider(selectedTabId))
|
||||
: null;
|
||||
child: Scaffold(
|
||||
bottomNavigationBar: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabInFullScreen = ref.watch(
|
||||
selectedTabStateProvider.select(
|
||||
(value) => value?.isFullScreen ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
final tabCount = ref.read(
|
||||
tabListProvider.select((tabs) => tabs.value.length),
|
||||
);
|
||||
return Visibility(
|
||||
visible: !tabInFullScreen,
|
||||
child: BrowserBottomAppBar(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: DragTarget<TabDragData>(
|
||||
onMove: (details) {
|
||||
ref
|
||||
.read(willAcceptDropProvider.notifier)
|
||||
.setData(DeleteDropData(details.data.tabId));
|
||||
},
|
||||
onLeave: (data) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
onAcceptWithDetails: (details) async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTab(details.data.tabId);
|
||||
},
|
||||
builder: (context, _, _) {
|
||||
return OverlayPortal(
|
||||
controller: overlayController,
|
||||
overlayChildBuilder: (context) {
|
||||
return displayedOverlay!;
|
||||
},
|
||||
child: Listener(
|
||||
onPointerDown:
|
||||
(displayedSheet != null)
|
||||
? (_) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
}
|
||||
: null,
|
||||
child: BackButtonListener(
|
||||
onBackButtonPressed: () async {
|
||||
final tabState =
|
||||
(selectedTabId != null)
|
||||
? ref.read(tabStateProvider(selectedTabId))
|
||||
: null;
|
||||
|
||||
//Don't do anything if a child route is active
|
||||
if (GoRouterState.of(context).topRoute?.path !=
|
||||
BrowserRoute().location) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (displayedSheet != null) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (displayedOverlay != null) {
|
||||
ref.read(overlayControllerProvider.notifier).dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabState?.isFullScreen == true) {
|
||||
await ref
|
||||
.read(selectedTabSessionNotifierProvider)
|
||||
.exitFullscreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabState?.historyState.canGoBack == true) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
final tabCount = ref.read(
|
||||
tabListProvider.select((tabs) => tabs.value.length),
|
||||
);
|
||||
|
||||
await controller.goBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
//Go router has routes to go back to
|
||||
if (context.canPop()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lastBackButtonPress.value != null &&
|
||||
DateTime.now().difference(lastBackButtonPress.value!) <
|
||||
const Duration(seconds: 2)) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
if (tabState != null && tabCount > 1) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTab(tabState.id);
|
||||
return true;
|
||||
} else {
|
||||
//Mark back as unhandled and navigator will pop
|
||||
//Don't do anything if a child route is active
|
||||
if (GoRouterState.of(context).topRoute?.name !=
|
||||
BrowserRoute.name) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
lastBackButtonPress.value = DateTime.now();
|
||||
ui_helper.showTabBackButtonMessage(context, tabCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
child: _BrowserView(displayedSheet: displayedSheet),
|
||||
if (displayedSheet != null) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (displayedOverlay != null) {
|
||||
ref.read(overlayControllerProvider.notifier).dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabState?.isFullScreen == true) {
|
||||
await ref
|
||||
.read(selectedTabSessionNotifierProvider)
|
||||
.exitFullscreen();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabState?.isLoading == true) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
|
||||
await controller.stopLoading();
|
||||
return true;
|
||||
} else if (tabState?.historyState.canGoBack == true) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
final controller = ref.read(
|
||||
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||
);
|
||||
|
||||
await controller.goBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
//Go router has routes to go back to
|
||||
if (context.canPop()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lastBackButtonPress.value != null &&
|
||||
DateTime.now().difference(
|
||||
lastBackButtonPress.value!,
|
||||
) <
|
||||
const Duration(seconds: 2)) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
if (tabState != null && tabCount > 1) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTab(tabState.id);
|
||||
return true;
|
||||
} else {
|
||||
//Mark back as unhandled and navigator will pop
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
lastBackButtonPress.value = DateTime.now();
|
||||
ui_helper.showTabBackButtonMessage(context, tabCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
child: _BrowserView(displayedSheet: displayedSheet),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: ReaderAppearanceButton(),
|
||||
bottomSheet:
|
||||
(displayedSheet != null)
|
||||
? NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.extent <= 0.1) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
return true;
|
||||
} else {
|
||||
ref
|
||||
.read(bottomSheetExtendProvider.notifier)
|
||||
.add(notification.extent);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: ReaderAppearanceButton(),
|
||||
bottomSheet:
|
||||
(displayedSheet != null)
|
||||
? NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: (notification) {
|
||||
if (notification.extent <= 0.1) {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.dismiss();
|
||||
return true;
|
||||
} else {
|
||||
ref
|
||||
.read(bottomSheetExtendProvider.notifier)
|
||||
.add(notification.extent);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
child: switch (displayedSheet) {
|
||||
ViewTabsSheet() => _ViewTabsSheet(
|
||||
key: ValueKey(displayedSheet),
|
||||
),
|
||||
final TabQaChatSheet parameter => _QaSheet(
|
||||
key: ValueKey(displayedSheet),
|
||||
chatId: parameter.chatId,
|
||||
),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
return false;
|
||||
},
|
||||
child: switch (displayedSheet) {
|
||||
ViewTabsSheet() => _ViewTabsSheet(
|
||||
key: ValueKey(displayedSheet),
|
||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||
),
|
||||
final TabQaChatSheet parameter => _QaSheet(
|
||||
key: ValueKey(displayedSheet),
|
||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||
chatId: parameter.chatId,
|
||||
),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -295,8 +325,9 @@ class _BrowserView extends StatelessWidget {
|
||||
|
||||
class _QaSheet extends HookConsumerWidget {
|
||||
final String chatId;
|
||||
final double maxChildSize;
|
||||
|
||||
const _QaSheet({super.key, required this.chatId});
|
||||
const _QaSheet({super.key, required this.chatId, this.maxChildSize = 1.0});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -306,7 +337,7 @@ class _QaSheet extends HookConsumerWidget {
|
||||
controller: draggableScrollableController,
|
||||
expand: false,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||
maxChildSize: maxChildSize,
|
||||
builder: (context, scrollController) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
@@ -349,7 +380,9 @@ class _QaSheet extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class _ViewTabsSheet extends HookConsumerWidget {
|
||||
const _ViewTabsSheet({super.key});
|
||||
final double maxChildSize;
|
||||
|
||||
const _ViewTabsSheet({super.key, this.maxChildSize = 1.0});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -359,7 +392,7 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
||||
controller: draggableScrollableController,
|
||||
expand: false,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||
maxChildSize: maxChildSize,
|
||||
builder: (context, scrollController) {
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
|
||||
+13
-2
@@ -311,11 +311,17 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
||||
).select((value) => value?.historyState),
|
||||
);
|
||||
|
||||
final isLoading = ref.watch(
|
||||
selectedTabStateProvider.select(
|
||||
(state) => state?.isLoading ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
(history?.canGoBack == true)
|
||||
(history?.canGoBack == true || isLoading)
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
final controller = ref.read(
|
||||
@@ -324,7 +330,12 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
||||
).notifier,
|
||||
);
|
||||
|
||||
await controller.goBack();
|
||||
if (isLoading) {
|
||||
await controller.stopLoading();
|
||||
} else {
|
||||
await controller.goBack();
|
||||
}
|
||||
|
||||
trippleDotMenuController.close();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
|
||||
+18
-10
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/logger.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
@@ -20,7 +21,9 @@ import 'package:lensai/features/geckoview/features/tabs/features/vector_store/do
|
||||
import 'package:lensai/features/user/domain/repositories/cache.dart';
|
||||
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:lensai/features/user/domain/services/local_authentication.dart';
|
||||
import 'package:lensai/features/web_feed/domain/providers/add_dialog_blocking.dart';
|
||||
import 'package:lensai/features/web_feed/domain/services/article_content_processor.dart';
|
||||
import 'package:lensai/presentation/hooks/on_initialization.dart';
|
||||
|
||||
class BrowserView extends StatefulHookConsumerWidget {
|
||||
final Duration screenshotPeriod;
|
||||
@@ -56,6 +59,16 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
useOnInitialization(() async {
|
||||
await ref.read(generalSettingsRepositoryProvider.notifier).fetch().then((
|
||||
settings,
|
||||
) {
|
||||
ref
|
||||
.read(browserDataServiceProvider.notifier)
|
||||
.deleteDataOnEngineStart(settings.deleteBrowsingDataOnQuit);
|
||||
});
|
||||
});
|
||||
|
||||
final hasTab = ref.watch(
|
||||
selectedTabProvider.select((value) => value != null),
|
||||
);
|
||||
@@ -84,7 +97,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
|
||||
ref.listen(feedRequestedProvider, (previous, next) async {
|
||||
if (next.valueOrNull.mapNotNull(Uri.tryParse) case final Uri url) {
|
||||
await FeedAddRoute($extra: url).push(context);
|
||||
if (GoRouterState.of(context).topRoute?.name != FeedAddRoute.name) {
|
||||
if (ref.read(addFeedDialogBlockingProvider.notifier).canPush(url)) {
|
||||
await FeedAddRoute($extra: url).push(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,15 +110,6 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
replacement: SizedBox.expand(child: Container(color: Colors.grey[800])),
|
||||
child: GeckoView(
|
||||
preInitializationStep: () async {
|
||||
await ref
|
||||
.read(generalSettingsRepositoryProvider.notifier)
|
||||
.fetch()
|
||||
.then((settings) {
|
||||
ref
|
||||
.read(browserDataServiceProvider.notifier)
|
||||
.deleteData(settings.deleteBrowsingDataOnQuit);
|
||||
});
|
||||
|
||||
await ref
|
||||
.read(eventServiceProvider)
|
||||
.viewReadyStateEvents
|
||||
|
||||
+43
-10
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -166,6 +167,20 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
int _calculateCrossAxisItemCount({
|
||||
required double screenWidth,
|
||||
required double horizontalPadding,
|
||||
required double crossAxisSpacing,
|
||||
}) {
|
||||
final totalHorizontalPadding = horizontalPadding * 2;
|
||||
final availableWidth =
|
||||
screenWidth - totalHorizontalPadding - crossAxisSpacing;
|
||||
|
||||
final crossAxisCount = availableWidth ~/ 180.0;
|
||||
|
||||
return crossAxisCount;
|
||||
}
|
||||
|
||||
double _calculateItemHeight({
|
||||
required double screenWidth,
|
||||
required double childAspectRatio,
|
||||
@@ -212,6 +227,25 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(
|
||||
() {
|
||||
final calculatedCount = _calculateCrossAxisItemCount(
|
||||
screenWidth: MediaQuery.of(context).size.width,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(calculatedCount, filteredTabIds.value.length),
|
||||
1,
|
||||
);
|
||||
},
|
||||
[
|
||||
MediaQuery.of(context).size.width,
|
||||
filteredTabIds.value.length,
|
||||
],
|
||||
);
|
||||
|
||||
final itemHeight = useMemoized(
|
||||
() => _calculateItemHeight(
|
||||
screenWidth: MediaQuery.of(context).size.width,
|
||||
@@ -219,9 +253,9 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
horizontalPadding: 4.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: 2,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
[MediaQuery.of(context).size.width],
|
||||
[MediaQuery.of(context).size.width, crossAxisCount],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
@@ -349,14 +383,13 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
builder: (children) {
|
||||
return GridView.builder(
|
||||
controller: sheetScrollController,
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: 2,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: children.length,
|
||||
itemBuilder: (context, index) => children[index],
|
||||
);
|
||||
|
||||
+51
-31
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/providers/format.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/extensions/uri.dart';
|
||||
@@ -80,6 +81,8 @@ class FeedSearch extends HookConsumerWidget {
|
||||
_ => null,
|
||||
};
|
||||
|
||||
final articleDate = article.updated ?? article.created;
|
||||
|
||||
return ListTile(
|
||||
leading: RepaintBoundary(
|
||||
child: UrlIcon([
|
||||
@@ -108,38 +111,55 @@ class FeedSearch extends HookConsumerWidget {
|
||||
article.displayTitle,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
subtitle:
|
||||
(searchSnippet.isNotEmpty)
|
||||
? MarkdownBody(
|
||||
data: searchSnippet!,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
a: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (searchSnippet.isNotEmpty)
|
||||
MarkdownBody(
|
||||
data: searchSnippet!,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
p: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: ((article.summaryPlain != null)
|
||||
? Text(
|
||||
article.summaryPlain!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: null),
|
||||
a: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
(article.summaryPlain != null)
|
||||
? Text(
|
||||
article.summaryPlain!,
|
||||
style: theme.textTheme.bodySmall,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
if (articleDate != null)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Text(
|
||||
ref
|
||||
.read(formatProvider.notifier)
|
||||
.fullDateTimeWithTimezone(articleDate),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
FeedArticleRoute(
|
||||
articleId: article.id,
|
||||
|
||||
+4
@@ -58,6 +58,10 @@ class TabSearch extends HookConsumerWidget {
|
||||
);
|
||||
});
|
||||
|
||||
if (tabs.isEmpty) {
|
||||
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||
}
|
||||
|
||||
return MultiSliver(
|
||||
children: [
|
||||
SliverToBoxAdapter(
|
||||
|
||||
@@ -52,6 +52,20 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<String> getContainerTabIds(String? containerId) {
|
||||
final query =
|
||||
selectOnly(db.tab)
|
||||
..addColumns([db.tab.id])
|
||||
..where(
|
||||
(containerId != null)
|
||||
? db.tab.containerId.equals(containerId)
|
||||
: db.tab.containerId.isNull(),
|
||||
)
|
||||
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
|
||||
|
||||
return query.map((row) => row.read(db.tab.id)!);
|
||||
}
|
||||
|
||||
SingleSelectable<String> generateLeadingOrderKey(
|
||||
String? containerId, {
|
||||
int bucket = 0,
|
||||
|
||||
@@ -20,20 +20,6 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
|
||||
db.tab.select()..where((t) => t.id.equals(id));
|
||||
|
||||
Selectable<String> containerTabIds(String? containerId) {
|
||||
final query =
|
||||
selectOnly(db.tab)
|
||||
..addColumns([db.tab.id])
|
||||
..where(
|
||||
(containerId != null)
|
||||
? db.tab.containerId.equals(containerId)
|
||||
: db.tab.containerId.isNull(),
|
||||
)
|
||||
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
|
||||
|
||||
return query.map((row) => row.read(db.tab.id)!);
|
||||
}
|
||||
|
||||
Selectable<String> getAllTabIds() {
|
||||
final query =
|
||||
selectOnly(db.tab)
|
||||
|
||||
@@ -43,7 +43,7 @@ Stream<List<String>> containerTabIds(Ref ref, ContainerFilter containerFilter) {
|
||||
|
||||
switch (containerFilter) {
|
||||
case ContainerFilterById(:final containerId):
|
||||
return db.tabDao.containerTabIds(containerId).watch();
|
||||
return db.containerDao.getContainerTabIds(containerId).watch();
|
||||
case ContainerFilterDisabled():
|
||||
return db.tabDao.getAllTabIds().watch();
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ class _MatchSortedContainersWithCountProviderElement
|
||||
(origin as MatchSortedContainersWithCountProvider).searchText;
|
||||
}
|
||||
|
||||
String _$containerTabIdsHash() => r'355c65523db29f376faa293ab1f1ed8273696548';
|
||||
String _$containerTabIdsHash() => r'61716a2ae74ffa590c1498d259382e69e24ad7f1';
|
||||
|
||||
/// See also [containerTabIds].
|
||||
@ProviderFor(containerTabIds)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
||||
@@ -48,13 +49,18 @@ class SelectedContainer extends _$SelectedContainer {
|
||||
}
|
||||
|
||||
if (passAuth) {
|
||||
state = id;
|
||||
|
||||
if (container.metadata.useProxy) {
|
||||
return SetContainerResult.successHasProxy;
|
||||
}
|
||||
final proxyPluginHealthy =
|
||||
await GeckoContainerProxyService().healthcheck();
|
||||
|
||||
return SetContainerResult.success;
|
||||
if (proxyPluginHealthy) {
|
||||
state = id;
|
||||
return SetContainerResult.successHasProxy;
|
||||
}
|
||||
} else {
|
||||
state = id;
|
||||
return SetContainerResult.success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ final selectedContainerDataProvider =
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
|
||||
String _$selectedContainerHash() => r'f3dab491337396b8d56daed37a0d85c85aa8d64a';
|
||||
String _$selectedContainerHash() => r'f3fbe035aff6902c8e140f02a3efeca20a75a70f';
|
||||
|
||||
/// See also [SelectedContainer].
|
||||
@ProviderFor(SelectedContainer)
|
||||
|
||||
@@ -15,6 +15,10 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
|
||||
}
|
||||
|
||||
Future<List<ContainerDataWithCount>> getAllContainersWithCount() {
|
||||
return ref.read(tabDatabaseProvider).containersWithCount().get();
|
||||
}
|
||||
|
||||
Future<void> replaceContainer(ContainerData container) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
@@ -30,6 +34,14 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<String>> getContainerTabIds(String? id) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.getContainerTabIds(id)
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> deleteContainer(String id) async {
|
||||
await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'container.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$containerRepositoryHash() =>
|
||||
r'7bb9aa1b2d88e79b7d5e69df1817fb4c97a2a542';
|
||||
r'9fff10b7896ea8865282bc315df21abde18b9d69';
|
||||
|
||||
/// See also [ContainerRepository].
|
||||
@ProviderFor(ContainerRepository)
|
||||
|
||||
@@ -24,8 +24,8 @@ class TabDataRepository extends _$TabDataRepository {
|
||||
final tabIds =
|
||||
await ref
|
||||
.read(tabDatabaseProvider)
|
||||
.tabDao
|
||||
.containerTabIds(containerId)
|
||||
.containerDao
|
||||
.getContainerTabIds(containerId)
|
||||
.get();
|
||||
|
||||
if (tabIds.isNotEmpty) {
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabDataRepositoryHash() => r'6c6f4aa5eaf4033541700bf18a592555a4b946d3';
|
||||
String _$tabDataRepositoryHash() => r'b5193e58aa331c7d0dc374e01032fe3a44955c99';
|
||||
|
||||
/// See also [TabDataRepository].
|
||||
@ProviderFor(TabDataRepository)
|
||||
|
||||
Reference in New Issue
Block a user