good state
This commit is contained in:
@@ -1,3 +1,9 @@
|
|||||||
|
//Required by background_fetch build
|
||||||
|
ext {
|
||||||
|
compileSdkVersion = 35
|
||||||
|
targetSdkVersion = 35
|
||||||
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.9.22'
|
ext.kotlin_version = '1.9.22'
|
||||||
}
|
}
|
||||||
@@ -6,6 +12,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
//Required by background_fetch build
|
||||||
maven { url "${project(':background_fetch').projectDir}/libs" }
|
maven { url "${project(':background_fetch').projectDir}/libs" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
part of 'routes.dart';
|
part of 'routes.dart';
|
||||||
|
|
||||||
@TypedGoRoute<BrowserRoute>(
|
@TypedGoRoute<BrowserRoute>(
|
||||||
name: 'BrowserRoute',
|
name: BrowserRoute.name,
|
||||||
path: '/',
|
path: '/',
|
||||||
routes: [
|
routes: [
|
||||||
TypedGoRoute<WebPageRoute>(name: 'WebPageRoute', path: 'page/:url'),
|
TypedGoRoute<WebPageRoute>(name: 'WebPageRoute', path: 'page/:url'),
|
||||||
@@ -28,6 +28,8 @@ part of 'routes.dart';
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
class BrowserRoute extends GoRouteData {
|
class BrowserRoute extends GoRouteData {
|
||||||
|
static const name = 'BrowserRoute';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
return const BrowserScreen();
|
return const BrowserScreen();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ part of 'routes.dart';
|
|||||||
name: 'FeedListRoute',
|
name: 'FeedListRoute',
|
||||||
path: '/feeds',
|
path: '/feeds',
|
||||||
routes: [
|
routes: [
|
||||||
TypedGoRoute<FeedAddRoute>(name: 'FeedAddRoute', path: 'add'),
|
TypedGoRoute<FeedAddRoute>(name: FeedAddRoute.name, path: 'add'),
|
||||||
TypedGoRoute<FeedArticleListRoute>(
|
TypedGoRoute<FeedArticleListRoute>(
|
||||||
name: 'FeedArticleListRoute',
|
name: 'FeedArticleListRoute',
|
||||||
path: 'articles/:feedId',
|
path: 'articles/:feedId',
|
||||||
@@ -73,6 +73,8 @@ class FeedEditRoute extends GoRouteData {
|
|||||||
class FeedAddRoute extends GoRouteData {
|
class FeedAddRoute extends GoRouteData {
|
||||||
final Uri? $extra;
|
final Uri? $extra;
|
||||||
|
|
||||||
|
static const name = 'FeedAddRoute';
|
||||||
|
|
||||||
const FeedAddRoute({this.$extra});
|
const FeedAddRoute({this.$extra});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
|
|
||||||
return WebPageInfo(
|
return WebPageInfo(
|
||||||
url: url,
|
url: url,
|
||||||
title: result['title'] as String?,
|
title: (result['title'] as String?)?.trim(),
|
||||||
favicon: favicon,
|
favicon: favicon,
|
||||||
feeds: Set.from(
|
feeds: Set.from(
|
||||||
(result['feeds']! as List<String>).map((url) => Uri.tryParse(url)),
|
(result['feeds']! as List<String>).map((url) => Uri.tryParse(url)),
|
||||||
@@ -257,6 +257,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<BrowserIcon?> getCachedIcon(Uri url) async {
|
Future<BrowserIcon?> getCachedIcon(Uri url) async {
|
||||||
|
if (url.scheme.startsWith('http')) {
|
||||||
final cachedBrowserIcon = _browserIconCache.get(url.origin);
|
final cachedBrowserIcon = _browserIconCache.get(url.origin);
|
||||||
if (cachedBrowserIcon != null) {
|
if (cachedBrowserIcon != null) {
|
||||||
return cachedBrowserIcon;
|
return cachedBrowserIcon;
|
||||||
@@ -273,6 +274,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'9bba5a6fc01aa788b2674db6ae3869065d6a528d';
|
r'9c8020d3c8d85342972db8934b37c53b31064b64';
|
||||||
|
|
||||||
/// See also [GenericWebsiteService].
|
/// See also [GenericWebsiteService].
|
||||||
@ProviderFor(GenericWebsiteService)
|
@ProviderFor(GenericWebsiteService)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class TabState extends WebPageInfo {
|
|||||||
required this.historyState,
|
required this.historyState,
|
||||||
required this.readerableState,
|
required this.readerableState,
|
||||||
required this.findResultState,
|
required this.findResultState,
|
||||||
}) : super(title: title);
|
}) : super(title: title.trim());
|
||||||
|
|
||||||
factory TabState.$default(String tabId) => TabState(
|
factory TabState.$default(String tabId) => TabState(
|
||||||
id: tabId,
|
id: tabId,
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class TabSession extends _$TabSession {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> stopLoading() {
|
||||||
|
return _sessionService.stopLoading();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> reload() {
|
Future<void> reload() {
|
||||||
return _sessionService.reload();
|
return _sessionService.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final selectedTabSessionNotifierProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<Raw<TabSession>>;
|
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<Raw<TabSession>>;
|
||||||
String _$tabSessionHash() => r'e4fbbcd430037cc01adb799783fb7974a11a27bf';
|
String _$tabSessionHash() => r'33d68f3860b3bc9a92db386535c7482bf4416008';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.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/extensions/nullable.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/entities/states/tab.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/data/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.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/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/features/share_intent/domain/entities/shared_content.dart';
|
||||||
import 'package:lensai/utils/debouncer.dart';
|
import 'package:lensai/utils/debouncer.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -80,15 +83,117 @@ class TabRepository extends _$TabRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectTab(String tabId) {
|
Future<bool> selectTab(String tabId) async {
|
||||||
return _tabsService.selectTab(tabId: tabId);
|
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> _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);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeTab(String tabId) {
|
|
||||||
return _tabsService.removeTab(tabId: 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);
|
return _tabsService.removeTabs(ids: tabIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'35c078a841d815aa2452c07b52c4cf0cbcec7e68';
|
String _$tabRepositoryHash() => r'a798385b50c101e41b9789e92181cbd5b6188246';
|
||||||
|
|
||||||
/// See also [TabRepository].
|
/// See also [TabRepository].
|
||||||
@ProviderFor(TabRepository)
|
@ProviderFor(TabRepository)
|
||||||
|
|||||||
@@ -4,10 +4,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
|
|
||||||
part 'browser_data.g.dart';
|
part 'browser_data.g.dart';
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod(keepAlive: true)
|
||||||
class BrowserDataService extends _$BrowserDataService {
|
class BrowserDataService extends _$BrowserDataService {
|
||||||
final _service = GeckoDeleteBrowserDataService();
|
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 {
|
Future<void> deleteData(Set<DeleteBrowsingDataType>? types) async {
|
||||||
if (types != null) {
|
if (types != null) {
|
||||||
for (final type in types) {
|
for (final type in types) {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ part of 'browser_data.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$browserDataServiceHash() =>
|
String _$browserDataServiceHash() =>
|
||||||
r'f56c60635505e06dbd5b05f1bcee9c25c4d0eaaf';
|
r'0a00db5b143d3851f2c171f4f939940f959b67bb';
|
||||||
|
|
||||||
/// See also [BrowserDataService].
|
/// See also [BrowserDataService].
|
||||||
@ProviderFor(BrowserDataService)
|
@ProviderFor(BrowserDataService)
|
||||||
final browserDataServiceProvider =
|
final browserDataServiceProvider =
|
||||||
AutoDisposeNotifierProvider<BrowserDataService, void>.internal(
|
NotifierProvider<BrowserDataService, void>.internal(
|
||||||
BrowserDataService.new,
|
BrowserDataService.new,
|
||||||
name: r'browserDataServiceProvider',
|
name: r'browserDataServiceProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -23,6 +23,6 @@ final browserDataServiceProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$BrowserDataService = AutoDisposeNotifier<void>;
|
typedef _$BrowserDataService = Notifier<void>;
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -61,6 +63,20 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
//We need this for BackButtonListener to work downstream
|
//We need this for BackButtonListener to work downstream
|
||||||
//No direct pop result will be handled here
|
//No direct pop result will be handled here
|
||||||
canPop: false,
|
canPop: false,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
bottomNavigationBar: Consumer(
|
bottomNavigationBar: Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
@@ -120,8 +136,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//Don't do anything if a child route is active
|
//Don't do anything if a child route is active
|
||||||
if (GoRouterState.of(context).topRoute?.path !=
|
if (GoRouterState.of(context).topRoute?.name !=
|
||||||
BrowserRoute().location) {
|
BrowserRoute.name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +160,16 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabState?.historyState.canGoBack == 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;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
@@ -161,7 +186,9 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastBackButtonPress.value != null &&
|
if (lastBackButtonPress.value != null &&
|
||||||
DateTime.now().difference(lastBackButtonPress.value!) <
|
DateTime.now().difference(
|
||||||
|
lastBackButtonPress.value!,
|
||||||
|
) <
|
||||||
const Duration(seconds: 2)) {
|
const Duration(seconds: 2)) {
|
||||||
lastBackButtonPress.value = null;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
@@ -208,15 +235,18 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
child: switch (displayedSheet) {
|
child: switch (displayedSheet) {
|
||||||
ViewTabsSheet() => _ViewTabsSheet(
|
ViewTabsSheet() => _ViewTabsSheet(
|
||||||
key: ValueKey(displayedSheet),
|
key: ValueKey(displayedSheet),
|
||||||
|
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||||
),
|
),
|
||||||
final TabQaChatSheet parameter => _QaSheet(
|
final TabQaChatSheet parameter => _QaSheet(
|
||||||
key: ValueKey(displayedSheet),
|
key: ValueKey(displayedSheet),
|
||||||
|
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
||||||
chatId: parameter.chatId,
|
chatId: parameter.chatId,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,8 +325,9 @@ class _BrowserView extends StatelessWidget {
|
|||||||
|
|
||||||
class _QaSheet extends HookConsumerWidget {
|
class _QaSheet extends HookConsumerWidget {
|
||||||
final String chatId;
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -306,7 +337,7 @@ class _QaSheet extends HookConsumerWidget {
|
|||||||
controller: draggableScrollableController,
|
controller: draggableScrollableController,
|
||||||
expand: false,
|
expand: false,
|
||||||
minChildSize: 0.1,
|
minChildSize: 0.1,
|
||||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
maxChildSize: maxChildSize,
|
||||||
builder: (context, scrollController) {
|
builder: (context, scrollController) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@@ -349,7 +380,9 @@ class _QaSheet extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ViewTabsSheet extends HookConsumerWidget {
|
class _ViewTabsSheet extends HookConsumerWidget {
|
||||||
const _ViewTabsSheet({super.key});
|
final double maxChildSize;
|
||||||
|
|
||||||
|
const _ViewTabsSheet({super.key, this.maxChildSize = 1.0});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -359,7 +392,7 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
|||||||
controller: draggableScrollableController,
|
controller: draggableScrollableController,
|
||||||
expand: false,
|
expand: false,
|
||||||
minChildSize: 0.1,
|
minChildSize: 0.1,
|
||||||
maxChildSize: MediaQuery.of(context).relativeSafeArea(),
|
maxChildSize: maxChildSize,
|
||||||
builder: (context, scrollController) {
|
builder: (context, scrollController) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
|
|||||||
+12
-1
@@ -311,11 +311,17 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
|||||||
).select((value) => value?.historyState),
|
).select((value) => value?.historyState),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final isLoading = ref.watch(
|
||||||
|
selectedTabStateProvider.select(
|
||||||
|
(state) => state?.isLoading ?? false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
(history?.canGoBack == true)
|
(history?.canGoBack == true || isLoading)
|
||||||
? IconButton(
|
? IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
@@ -324,7 +330,12 @@ class BrowserBottomAppBar extends HookConsumerWidget {
|
|||||||
).notifier,
|
).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
await controller.stopLoading();
|
||||||
|
} else {
|
||||||
await controller.goBack();
|
await controller.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
trippleDotMenuController.close();
|
trippleDotMenuController.close();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
|||||||
+17
-9
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/logger.dart';
|
import 'package:lensai/core/logger.dart';
|
||||||
import 'package:lensai/core/routing/routes.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/cache.dart';
|
||||||
import 'package:lensai/features/user/domain/repositories/general_settings.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/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/features/web_feed/domain/services/article_content_processor.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/on_initialization.dart';
|
||||||
|
|
||||||
class BrowserView extends StatefulHookConsumerWidget {
|
class BrowserView extends StatefulHookConsumerWidget {
|
||||||
final Duration screenshotPeriod;
|
final Duration screenshotPeriod;
|
||||||
@@ -56,6 +59,16 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
final hasTab = ref.watch(
|
||||||
selectedTabProvider.select((value) => value != null),
|
selectedTabProvider.select((value) => value != null),
|
||||||
);
|
);
|
||||||
@@ -84,8 +97,12 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
|
|
||||||
ref.listen(feedRequestedProvider, (previous, next) async {
|
ref.listen(feedRequestedProvider, (previous, next) async {
|
||||||
if (next.valueOrNull.mapNotNull(Uri.tryParse) case final Uri url) {
|
if (next.valueOrNull.mapNotNull(Uri.tryParse) case final Uri url) {
|
||||||
|
if (GoRouterState.of(context).topRoute?.name != FeedAddRoute.name) {
|
||||||
|
if (ref.read(addFeedDialogBlockingProvider.notifier).canPush(url)) {
|
||||||
await FeedAddRoute($extra: url).push(context);
|
await FeedAddRoute($extra: url).push(context);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Visibility(
|
return Visibility(
|
||||||
@@ -93,15 +110,6 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
replacement: SizedBox.expand(child: Container(color: Colors.grey[800])),
|
replacement: SizedBox.expand(child: Container(color: Colors.grey[800])),
|
||||||
child: GeckoView(
|
child: GeckoView(
|
||||||
preInitializationStep: () async {
|
preInitializationStep: () async {
|
||||||
await ref
|
|
||||||
.read(generalSettingsRepositoryProvider.notifier)
|
|
||||||
.fetch()
|
|
||||||
.then((settings) {
|
|
||||||
ref
|
|
||||||
.read(browserDataServiceProvider.notifier)
|
|
||||||
.deleteData(settings.deleteBrowsingDataOnQuit);
|
|
||||||
});
|
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(eventServiceProvider)
|
.read(eventServiceProvider)
|
||||||
.viewReadyStateEvents
|
.viewReadyStateEvents
|
||||||
|
|||||||
+38
-5
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -166,6 +167,20 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
super.key,
|
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({
|
double _calculateItemHeight({
|
||||||
required double screenWidth,
|
required double screenWidth,
|
||||||
required double childAspectRatio,
|
required double childAspectRatio,
|
||||||
@@ -212,6 +227,25 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
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(
|
final itemHeight = useMemoized(
|
||||||
() => _calculateItemHeight(
|
() => _calculateItemHeight(
|
||||||
screenWidth: MediaQuery.of(context).size.width,
|
screenWidth: MediaQuery.of(context).size.width,
|
||||||
@@ -219,9 +253,9 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
horizontalPadding: 4.0,
|
horizontalPadding: 4.0,
|
||||||
mainAxisSpacing: 8.0,
|
mainAxisSpacing: 8.0,
|
||||||
crossAxisSpacing: 8.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: crossAxisCount,
|
||||||
),
|
),
|
||||||
[MediaQuery.of(context).size.width],
|
[MediaQuery.of(context).size.width, crossAxisCount],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@@ -349,13 +383,12 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
|||||||
builder: (children) {
|
builder: (children) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
controller: sheetScrollController,
|
controller: sheetScrollController,
|
||||||
gridDelegate:
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
childAspectRatio: 0.75,
|
childAspectRatio: 0.75,
|
||||||
mainAxisSpacing: 8.0,
|
mainAxisSpacing: 8.0,
|
||||||
crossAxisSpacing: 8.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: crossAxisCount,
|
||||||
),
|
),
|
||||||
itemCount: children.length,
|
itemCount: children.length,
|
||||||
itemBuilder: (context, index) => children[index],
|
itemBuilder: (context, index) => children[index],
|
||||||
|
|||||||
+25
-5
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/core/routing/routes.dart';
|
||||||
import 'package:lensai/extensions/nullable.dart';
|
import 'package:lensai/extensions/nullable.dart';
|
||||||
import 'package:lensai/extensions/uri.dart';
|
import 'package:lensai/extensions/uri.dart';
|
||||||
@@ -80,6 +81,8 @@ class FeedSearch extends HookConsumerWidget {
|
|||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final articleDate = article.updated ?? article.created;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child: UrlIcon([
|
child: UrlIcon([
|
||||||
@@ -108,9 +111,11 @@ class FeedSearch extends HookConsumerWidget {
|
|||||||
article.displayTitle,
|
article.displayTitle,
|
||||||
style: theme.textTheme.titleMedium,
|
style: theme.textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
subtitle:
|
subtitle: Column(
|
||||||
(searchSnippet.isNotEmpty)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
? MarkdownBody(
|
children: [
|
||||||
|
if (searchSnippet.isNotEmpty)
|
||||||
|
MarkdownBody(
|
||||||
data: searchSnippet!,
|
data: searchSnippet!,
|
||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
p: Theme.of(
|
p: Theme.of(
|
||||||
@@ -132,14 +137,29 @@ class FeedSearch extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ((article.summaryPlain != null)
|
else
|
||||||
|
(article.summaryPlain != null)
|
||||||
? Text(
|
? Text(
|
||||||
article.summaryPlain!,
|
article.summaryPlain!,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: null),
|
: 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: () {
|
onTap: () {
|
||||||
FeedArticleRoute(
|
FeedArticleRoute(
|
||||||
articleId: article.id,
|
articleId: article.id,
|
||||||
|
|||||||
+4
@@ -58,6 +58,10 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (tabs.isEmpty) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
}
|
||||||
|
|
||||||
return MultiSliver(
|
return MultiSliver(
|
||||||
children: [
|
children: [
|
||||||
SliverToBoxAdapter(
|
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(
|
SingleSelectable<String> generateLeadingOrderKey(
|
||||||
String? containerId, {
|
String? containerId, {
|
||||||
int bucket = 0,
|
int bucket = 0,
|
||||||
|
|||||||
@@ -20,20 +20,6 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
|
SingleOrNullSelectable<TabData> getTabDataById(String id) =>
|
||||||
db.tab.select()..where((t) => t.id.equals(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() {
|
Selectable<String> getAllTabIds() {
|
||||||
final query =
|
final query =
|
||||||
selectOnly(db.tab)
|
selectOnly(db.tab)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Stream<List<String>> containerTabIds(Ref ref, ContainerFilter containerFilter) {
|
|||||||
|
|
||||||
switch (containerFilter) {
|
switch (containerFilter) {
|
||||||
case ContainerFilterById(:final containerId):
|
case ContainerFilterById(:final containerId):
|
||||||
return db.tabDao.containerTabIds(containerId).watch();
|
return db.containerDao.getContainerTabIds(containerId).watch();
|
||||||
case ContainerFilterDisabled():
|
case ContainerFilterDisabled():
|
||||||
return db.tabDao.getAllTabIds().watch();
|
return db.tabDao.getAllTabIds().watch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class _MatchSortedContainersWithCountProviderElement
|
|||||||
(origin as MatchSortedContainersWithCountProvider).searchText;
|
(origin as MatchSortedContainersWithCountProvider).searchText;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerTabIdsHash() => r'355c65523db29f376faa293ab1f1ed8273696548';
|
String _$containerTabIdsHash() => r'61716a2ae74ffa590c1498d259382e69e24ad7f1';
|
||||||
|
|
||||||
/// See also [containerTabIds].
|
/// See also [containerTabIds].
|
||||||
@ProviderFor(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/models/container_data.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
@@ -48,15 +49,20 @@ class SelectedContainer extends _$SelectedContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (passAuth) {
|
if (passAuth) {
|
||||||
state = id;
|
|
||||||
|
|
||||||
if (container.metadata.useProxy) {
|
if (container.metadata.useProxy) {
|
||||||
|
final proxyPluginHealthy =
|
||||||
|
await GeckoContainerProxyService().healthcheck();
|
||||||
|
|
||||||
|
if (proxyPluginHealthy) {
|
||||||
|
state = id;
|
||||||
return SetContainerResult.successHasProxy;
|
return SetContainerResult.successHasProxy;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
state = id;
|
||||||
return SetContainerResult.success;
|
return SetContainerResult.success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SetContainerResult.failed;
|
return SetContainerResult.failed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final selectedContainerDataProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
|
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
|
||||||
String _$selectedContainerHash() => r'f3dab491337396b8d56daed37a0d85c85aa8d64a';
|
String _$selectedContainerHash() => r'f3fbe035aff6902c8e140f02a3efeca20a75a70f';
|
||||||
|
|
||||||
/// See also [SelectedContainer].
|
/// See also [SelectedContainer].
|
||||||
@ProviderFor(SelectedContainer)
|
@ProviderFor(SelectedContainer)
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
|
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<ContainerDataWithCount>> getAllContainersWithCount() {
|
||||||
|
return ref.read(tabDatabaseProvider).containersWithCount().get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> replaceContainer(ContainerData container) {
|
Future<void> replaceContainer(ContainerData container) {
|
||||||
return ref
|
return ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
@@ -30,6 +34,14 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getContainerTabIds(String? id) {
|
||||||
|
return ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.containerDao
|
||||||
|
.getContainerTabIds(id)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteContainer(String id) async {
|
Future<void> deleteContainer(String id) async {
|
||||||
await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id);
|
await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'container.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$containerRepositoryHash() =>
|
String _$containerRepositoryHash() =>
|
||||||
r'7bb9aa1b2d88e79b7d5e69df1817fb4c97a2a542';
|
r'9fff10b7896ea8865282bc315df21abde18b9d69';
|
||||||
|
|
||||||
/// See also [ContainerRepository].
|
/// See also [ContainerRepository].
|
||||||
@ProviderFor(ContainerRepository)
|
@ProviderFor(ContainerRepository)
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
final tabIds =
|
final tabIds =
|
||||||
await ref
|
await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
.tabDao
|
.containerDao
|
||||||
.containerTabIds(containerId)
|
.getContainerTabIds(containerId)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (tabIds.isNotEmpty) {
|
if (tabIds.isNotEmpty) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'6c6f4aa5eaf4033541700bf18a592555a4b946d3';
|
String _$tabDataRepositoryHash() => r'b5193e58aa331c7d0dc374e01032fe3a44955c99';
|
||||||
|
|
||||||
/// See also [TabDataRepository].
|
/// See also [TabDataRepository].
|
||||||
@ProviderFor(TabDataRepository)
|
@ProviderFor(TabDataRepository)
|
||||||
|
|||||||
@@ -975,6 +975,7 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
|
|||||||
contentMarkdown,
|
contentMarkdown,
|
||||||
contentPlain,
|
contentPlain,
|
||||||
icon,
|
icon,
|
||||||
|
siteLink,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? entityName;
|
String get aliasedName => _alias ?? entityName;
|
||||||
@@ -983,7 +984,7 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
|
|||||||
@override
|
@override
|
||||||
Map<SqlDialect, String> get createViewStatements => {
|
Map<SqlDialect, String> get createViewStatements => {
|
||||||
SqlDialect.sqlite:
|
SqlDialect.sqlite:
|
||||||
'CREATE VIEW article_view AS SELECT a.*, f.icon FROM article AS a INNER JOIN feed AS f ON f.url = a.feed_id',
|
'CREATE VIEW article_view AS SELECT a.*, f.icon, f.site_link FROM article AS a INNER JOIN feed AS f ON f.url = a.feed_id',
|
||||||
};
|
};
|
||||||
@override
|
@override
|
||||||
ArticleView get asDslTable => this;
|
ArticleView get asDslTable => this;
|
||||||
@@ -1071,6 +1072,12 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
|
|||||||
data['${effectivePrefix}icon'],
|
data['${effectivePrefix}icon'],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
siteLink: Feed.$convertersiteLinkn.fromSql(
|
||||||
|
attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}site_link'],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,6 +1188,13 @@ class ArticleView extends ViewInfo<ArticleView, FeedArticle>
|
|||||||
true,
|
true,
|
||||||
type: DriftSqlType.string,
|
type: DriftSqlType.string,
|
||||||
).withConverter<Uri?>(Feed.$convertericonn);
|
).withConverter<Uri?>(Feed.$convertericonn);
|
||||||
|
late final GeneratedColumnWithTypeConverter<Uri?, String> siteLink =
|
||||||
|
GeneratedColumn<String>(
|
||||||
|
'site_link',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
).withConverter<Uri?>(Feed.$convertersiteLinkn);
|
||||||
@override
|
@override
|
||||||
ArticleView createAlias(String alias) {
|
ArticleView createAlias(String alias) {
|
||||||
return ArticleView(attachedDatabase, alias);
|
return ArticleView(attachedDatabase, alias);
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ abstract class _$FeedArticleCWProxy {
|
|||||||
|
|
||||||
FeedArticle icon(Uri? icon);
|
FeedArticle icon(Uri? icon);
|
||||||
|
|
||||||
|
FeedArticle siteLink(Uri? siteLink);
|
||||||
|
|
||||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||||
///
|
///
|
||||||
/// Usage
|
/// Usage
|
||||||
@@ -65,6 +67,7 @@ abstract class _$FeedArticleCWProxy {
|
|||||||
String? contentMarkdown,
|
String? contentMarkdown,
|
||||||
String? contentPlain,
|
String? contentPlain,
|
||||||
Uri? icon,
|
Uri? icon,
|
||||||
|
Uri? siteLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +134,9 @@ class _$FeedArticleCWProxyImpl implements _$FeedArticleCWProxy {
|
|||||||
@override
|
@override
|
||||||
FeedArticle icon(Uri? icon) => this(icon: icon);
|
FeedArticle icon(Uri? icon) => this(icon: icon);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FeedArticle siteLink(Uri? siteLink) => this(siteLink: siteLink);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `FeedArticle(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||||
///
|
///
|
||||||
@@ -156,6 +162,7 @@ class _$FeedArticleCWProxyImpl implements _$FeedArticleCWProxy {
|
|||||||
Object? contentMarkdown = const $CopyWithPlaceholder(),
|
Object? contentMarkdown = const $CopyWithPlaceholder(),
|
||||||
Object? contentPlain = const $CopyWithPlaceholder(),
|
Object? contentPlain = const $CopyWithPlaceholder(),
|
||||||
Object? icon = const $CopyWithPlaceholder(),
|
Object? icon = const $CopyWithPlaceholder(),
|
||||||
|
Object? siteLink = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return FeedArticle(
|
return FeedArticle(
|
||||||
id:
|
id:
|
||||||
@@ -243,6 +250,11 @@ class _$FeedArticleCWProxyImpl implements _$FeedArticleCWProxy {
|
|||||||
? _value.icon
|
? _value.icon
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: icon as Uri?,
|
: icon as Uri?,
|
||||||
|
siteLink:
|
||||||
|
siteLink == const $CopyWithPlaceholder()
|
||||||
|
? _value.siteLink
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: siteLink as Uri?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,6 +305,8 @@ FeedArticle _$FeedArticleFromJson(Map<String, dynamic> json) => FeedArticle(
|
|||||||
contentMarkdown: json['contentMarkdown'] as String?,
|
contentMarkdown: json['contentMarkdown'] as String?,
|
||||||
contentPlain: json['contentPlain'] as String?,
|
contentPlain: json['contentPlain'] as String?,
|
||||||
icon: json['icon'] == null ? null : Uri.parse(json['icon'] as String),
|
icon: json['icon'] == null ? null : Uri.parse(json['icon'] as String),
|
||||||
|
siteLink:
|
||||||
|
json['siteLink'] == null ? null : Uri.parse(json['siteLink'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
|
Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
|
||||||
@@ -314,4 +328,5 @@ Map<String, dynamic> _$FeedArticleToJson(FeedArticle instance) =>
|
|||||||
'contentMarkdown': instance.contentMarkdown,
|
'contentMarkdown': instance.contentMarkdown,
|
||||||
'contentPlain': instance.contentPlain,
|
'contentPlain': instance.contentPlain,
|
||||||
'icon': instance.icon?.toString(),
|
'icon': instance.icon?.toString(),
|
||||||
|
'siteLink': instance.siteLink?.toString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ class _FetchWebFeedProviderElement
|
|||||||
Uri get url => (origin as FetchWebFeedProvider).url;
|
Uri get url => (origin as FetchWebFeedProvider).url;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$articleSearchHash() => r'8bf2c4aa8d8b3918be8a416909535682abfbab35';
|
String _$articleSearchHash() => r'cc59a5d3c4b926db9f03ed96db592965ff925b04';
|
||||||
|
|
||||||
abstract class _$ArticleSearch
|
abstract class _$ArticleSearch
|
||||||
extends BuildlessAutoDisposeStreamNotifier<List<FeedArticle>> {
|
extends BuildlessAutoDisposeStreamNotifier<List<FeedArticle>> {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:lensai/core/logger.dart';
|
||||||
|
import 'package:lensai/extensions/nullable.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'add_dialog_blocking.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class AddFeedDialogBlocking extends _$AddFeedDialogBlocking {
|
||||||
|
DateTime? _lastIgnore;
|
||||||
|
final _ignoredUrls = <Uri, DateTime>{};
|
||||||
|
|
||||||
|
void ignore(Uri url) {
|
||||||
|
final date = DateTime.now();
|
||||||
|
|
||||||
|
_lastIgnore = date;
|
||||||
|
_ignoredUrls[url] = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canPush(Uri url) {
|
||||||
|
if (_lastIgnore.mapNotNull(
|
||||||
|
(last) =>
|
||||||
|
DateTime.now().difference(last) <= const Duration(seconds: 30),
|
||||||
|
) ??
|
||||||
|
false) {
|
||||||
|
logger.i('Blocking add feed default timeout for $url');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ignoredUrls[url].mapNotNull(
|
||||||
|
(last) =>
|
||||||
|
DateTime.now().difference(last) <= const Duration(minutes: 5),
|
||||||
|
) ??
|
||||||
|
false) {
|
||||||
|
logger.i('Blocking add feed url specific for $url');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void build() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'add_dialog_blocking.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$addFeedDialogBlockingHash() =>
|
||||||
|
r'513071d5e507dd292df6b4a3ee3ef7d2217db5bb';
|
||||||
|
|
||||||
|
/// See also [AddFeedDialogBlocking].
|
||||||
|
@ProviderFor(AddFeedDialogBlocking)
|
||||||
|
final addFeedDialogBlockingProvider =
|
||||||
|
NotifierProvider<AddFeedDialogBlocking, void>.internal(
|
||||||
|
AddFeedDialogBlocking.new,
|
||||||
|
name: r'addFeedDialogBlockingProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$addFeedDialogBlockingHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AddFeedDialogBlocking = 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,7 +1,6 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:lensai/core/logger.dart';
|
import 'package:lensai/core/logger.dart';
|
||||||
import 'package:lensai/extensions/nullable.dart';
|
|
||||||
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
import 'package:lensai/features/web_feed/data/models/feed_article.dart';
|
||||||
import 'package:lensai/features/web_feed/data/providers.dart';
|
import 'package:lensai/features/web_feed/data/providers.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -29,10 +28,10 @@ class ArticleContentProcessorService extends _$ArticleContentProcessorService {
|
|||||||
articles
|
articles
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(index, article) => article.copyWith(
|
(index, article) => article.copyWith(
|
||||||
contentMarkdown: content[index].markdown.whenNotEmpty,
|
contentMarkdown: content[index].markdown ?? '',
|
||||||
contentPlain: content[index].plain.whenNotEmpty,
|
contentPlain: content[index].plain,
|
||||||
summaryMarkdown: summary[index].markdown.whenNotEmpty,
|
summaryMarkdown: summary[index].markdown ?? '',
|
||||||
summaryPlain: summary[index].plain.whenNotEmpty,
|
summaryPlain: summary[index].plain,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'article_content_processor.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$articleContentProcessorServiceHash() =>
|
String _$articleContentProcessorServiceHash() =>
|
||||||
r'182cd89e38a0a9a86b7425c16ad2bd1fc39c6704';
|
r'e7cc3da71f6dcf39b0c10df4dcd061f816d5c12e';
|
||||||
|
|
||||||
/// See also [ArticleContentProcessorService].
|
/// See also [ArticleContentProcessorService].
|
||||||
@ProviderFor(ArticleContentProcessorService)
|
@ProviderFor(ArticleContentProcessorService)
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ extension ParseRssCategory on List<RssCategory> {
|
|||||||
List<FeedCategory> toFeedCategories() {
|
List<FeedCategory> toFeedCategories() {
|
||||||
return where((category) => category.value.isNotEmpty)
|
return where((category) => category.value.isNotEmpty)
|
||||||
.map(
|
.map(
|
||||||
(category) =>
|
(category) => FeedCategory(
|
||||||
FeedCategory(id: '${category.domain} ${category.value}'.trim()),
|
id: '${category.domain ?? ''} ${category.value ?? ''}'.trim(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/routing/routes.dart';
|
import 'package:lensai/core/routing/routes.dart';
|
||||||
|
import 'package:lensai/features/web_feed/domain/providers/add_dialog_blocking.dart';
|
||||||
import 'package:lensai/utils/form_validators.dart';
|
import 'package:lensai/utils/form_validators.dart';
|
||||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||||
|
|
||||||
@@ -38,6 +39,16 @@ class AddFeedDialog extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (initialUri != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(addFeedDialogBlockingProvider.notifier)
|
||||||
|
.ignore(initialUri!);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
child: const Text('Ignore'),
|
||||||
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:background_fetch/background_fetch.dart';
|
import 'package:background_fetch/background_fetch.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
||||||
|
show GeckoBrowserService;
|
||||||
import 'package:home_widget/home_widget.dart';
|
import 'package:home_widget/home_widget.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/error_observer.dart';
|
import 'package:lensai/core/error_observer.dart';
|
||||||
@@ -16,6 +18,8 @@ import 'package:lensai/presentation/main_app.dart';
|
|||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
await GeckoBrowserService().initialize();
|
||||||
|
|
||||||
await BackgroundFetch.registerHeadlessTask(backgroundFetch);
|
await BackgroundFetch.registerHeadlessTask(backgroundFetch);
|
||||||
|
|
||||||
await HomeWidget.setAppGroupId('bang_navigator');
|
await HomeWidget.setAppGroupId('bang_navigator');
|
||||||
|
|||||||
+5
-167
@@ -1,171 +1,20 @@
|
|||||||
package eu.lensai.flutter_mozilla_components
|
package eu.lensai.flutter_mozilla_components
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
|
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoContainerProxyApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoDownloadsApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoPrefApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoSuggestionApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserExtensionApiImpl
|
|
||||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDownloadsApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
|
||||||
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
import mozilla.components.support.base.log.Log
|
|
||||||
import mozilla.components.support.base.log.sink.AndroidLogSink
|
|
||||||
|
|
||||||
|
|
||||||
/** FlutterMozillaComponentsPlugin */
|
/** FlutterMozillaComponentsPlugin */
|
||||||
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||||
companion object {
|
private val browserApi = GeckoBrowserApiImpl()
|
||||||
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
|
|
||||||
|
|
||||||
private var isGeckoInitialized = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private val components by lazy {
|
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
|
|
||||||
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding;
|
|
||||||
private lateinit var _flutterEvents : GeckoStateEvents
|
|
||||||
|
|
||||||
private var isPlatformViewRegistered = false
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
synchronized(this) {
|
browserApi.attachBinding(flutterPluginBinding)
|
||||||
if(!isGeckoInitialized) {
|
GeckoBrowserApi.setUp(flutterPluginBinding.binaryMessenger, browserApi)
|
||||||
Log.addSink(AndroidLogSink())
|
|
||||||
|
|
||||||
setupGeckoEngine(flutterPluginBinding)
|
|
||||||
isGeckoInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupGeckoEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
_flutterPluginBinding = flutterPluginBinding
|
|
||||||
|
|
||||||
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
|
|
||||||
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents)
|
|
||||||
|
|
||||||
val readerViewController =
|
|
||||||
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
|
||||||
|
|
||||||
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
|
|
||||||
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
|
|
||||||
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
|
|
||||||
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
|
|
||||||
|
|
||||||
GlobalComponents.setUp(
|
|
||||||
flutterPluginBinding.applicationContext,
|
|
||||||
_flutterEvents,
|
|
||||||
readerViewController,
|
|
||||||
selectionActionDelegate,
|
|
||||||
addonEvents,
|
|
||||||
tabContentEvents,
|
|
||||||
extensionEvents
|
|
||||||
)
|
|
||||||
|
|
||||||
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
|
|
||||||
showNativeFragment()
|
|
||||||
})
|
|
||||||
|
|
||||||
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
|
|
||||||
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(flutterPluginBinding.applicationContext))
|
|
||||||
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
|
|
||||||
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
|
|
||||||
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
|
|
||||||
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
|
|
||||||
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
|
|
||||||
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
|
|
||||||
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
|
|
||||||
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
|
|
||||||
selectionActionDelegate
|
|
||||||
))
|
|
||||||
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
|
|
||||||
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
|
|
||||||
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
|
|
||||||
|
|
||||||
ReaderViewEvents.setUp(
|
|
||||||
_flutterPluginBinding.binaryMessenger,
|
|
||||||
components.events.readerViewEvents
|
|
||||||
)
|
|
||||||
|
|
||||||
val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java)
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
flutterPluginBinding.applicationContext.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNativeFragment(): Boolean {
|
|
||||||
if (!isPlatformViewRegistered) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activity == null || activity !is FragmentActivity) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val fragmentActivity = activity as FragmentActivity
|
|
||||||
|
|
||||||
// Check if the container view exists in the view hierarchy
|
|
||||||
val container = fragmentActivity.findViewById<View>(FRAGMENT_CONTAINER_ID)
|
|
||||||
if (container == null) {
|
|
||||||
// Container doesn't exist yet, retry later
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val nativeFragment = BrowserFragment.create()
|
|
||||||
val fm = fragmentActivity.supportFragmentManager
|
|
||||||
fm.beginTransaction()
|
|
||||||
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
@@ -173,17 +22,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
this.activity = binding.activity
|
browserApi.attachActivity(binding.activity)
|
||||||
|
|
||||||
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
|
||||||
"eu.lensai/gecko", GeckoViewFactory(
|
|
||||||
binding.activity,
|
|
||||||
FRAGMENT_CONTAINER_ID,
|
|
||||||
_flutterEvents
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
isPlatformViewRegistered = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
@@ -195,7 +34,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
override fun onDetachedFromActivity() {
|
||||||
this.activity = null
|
browserApi.detachActivity()
|
||||||
isPlatformViewRegistered = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-1
@@ -1,17 +1,99 @@
|
|||||||
package eu.lensai.flutter_mozilla_components.api
|
package eu.lensai.flutter_mozilla_components.api
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import eu.lensai.flutter_mozilla_components.BrowserFragment
|
||||||
|
import eu.lensai.flutter_mozilla_components.GeckoViewFactory
|
||||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||||
|
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
||||||
|
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDownloadsApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
|
||||||
import mozilla.components.browser.state.action.SystemAction
|
import mozilla.components.browser.state.action.SystemAction
|
||||||
import mozilla.components.feature.addons.logger
|
import mozilla.components.feature.addons.logger
|
||||||
|
import mozilla.components.support.base.log.Log
|
||||||
|
import mozilla.components.support.base.log.sink.AndroidLogSink
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of GeckoBrowserApi that handles browser-related operations
|
* Implementation of GeckoBrowserApi that handles browser-related operations
|
||||||
* @param showFragmentCallback Callback function to show native fragment
|
* @param showFragmentCallback Callback function to show native fragment
|
||||||
*/
|
*/
|
||||||
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : GeckoBrowserApi {
|
class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "GeckoBrowserApiImpl"
|
private const val TAG = "GeckoBrowserApiImpl"
|
||||||
|
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
|
||||||
|
|
||||||
|
private var isGeckoInitialized = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val components by lazy {
|
||||||
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activity: Activity? = null
|
||||||
|
private var isPlatformViewRegistered = false
|
||||||
|
|
||||||
|
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
|
||||||
|
private lateinit var _flutterEvents : GeckoStateEvents
|
||||||
|
|
||||||
|
fun attachBinding(flutterPluginBinding: FlutterPluginBinding) {
|
||||||
|
_flutterPluginBinding = flutterPluginBinding
|
||||||
|
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachActivity(activity: Activity) {
|
||||||
|
this.activity = activity
|
||||||
|
|
||||||
|
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
||||||
|
"eu.lensai/gecko", GeckoViewFactory(
|
||||||
|
activity,
|
||||||
|
FRAGMENT_CONTAINER_ID,
|
||||||
|
_flutterEvents
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
isPlatformViewRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun detachActivity() {
|
||||||
|
this.activity = null
|
||||||
|
isPlatformViewRegistered = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
synchronized(this) {
|
||||||
|
if(!isGeckoInitialized) {
|
||||||
|
Log.addSink(AndroidLogSink())
|
||||||
|
|
||||||
|
setupGeckoEngine()
|
||||||
|
isGeckoInitialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showNativeFragment(): Boolean {
|
override fun showNativeFragment(): Boolean {
|
||||||
@@ -24,6 +106,85 @@ class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : Gec
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupGeckoEngine() {
|
||||||
|
val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
|
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents)
|
||||||
|
|
||||||
|
val readerViewController =
|
||||||
|
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
|
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
|
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
|
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
|
||||||
|
|
||||||
|
GlobalComponents.setUp(
|
||||||
|
_flutterPluginBinding.applicationContext,
|
||||||
|
_flutterEvents,
|
||||||
|
readerViewController,
|
||||||
|
selectionActionDelegate,
|
||||||
|
addonEvents,
|
||||||
|
tabContentEvents,
|
||||||
|
extensionEvents
|
||||||
|
)
|
||||||
|
|
||||||
|
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
|
||||||
|
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(_flutterPluginBinding.applicationContext))
|
||||||
|
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
|
||||||
|
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
|
||||||
|
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
|
||||||
|
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
|
||||||
|
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
|
||||||
|
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
|
||||||
|
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
|
||||||
|
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
|
||||||
|
selectionActionDelegate
|
||||||
|
))
|
||||||
|
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
|
||||||
|
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
|
||||||
|
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
|
||||||
|
|
||||||
|
ReaderViewEvents.setUp(
|
||||||
|
_flutterPluginBinding.binaryMessenger,
|
||||||
|
components.events.readerViewEvents
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(_flutterPluginBinding.applicationContext, NotificationActivity::class.java)
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
_flutterPluginBinding.applicationContext.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFragmentCallback(): Boolean {
|
||||||
|
if (!isPlatformViewRegistered) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity == null || activity !is FragmentActivity) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentActivity = activity as FragmentActivity
|
||||||
|
|
||||||
|
// Check if the container view exists in the view hierarchy
|
||||||
|
val container = fragmentActivity.findViewById<View>(FRAGMENT_CONTAINER_ID)
|
||||||
|
if (container == null) {
|
||||||
|
// Container doesn't exist yet, retry later
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val nativeFragment = BrowserFragment.create()
|
||||||
|
val fm = fragmentActivity.supportFragmentManager
|
||||||
|
fm.beginTransaction()
|
||||||
|
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTrimMemory(level: Long) {
|
override fun onTrimMemory(level: Long) {
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
|
|
||||||
|
|||||||
+17
@@ -1,7 +1,10 @@
|
|||||||
package eu.lensai.flutter_mozilla_components.api
|
package eu.lensai.flutter_mozilla_components.api
|
||||||
|
|
||||||
|
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
|
||||||
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
|
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
|
||||||
|
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
||||||
override fun setProxyPort(port: Long) {
|
override fun setProxyPort(port: Long) {
|
||||||
@@ -15,4 +18,18 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
|||||||
override fun removeContainerProxy(contextId: String) {
|
override fun removeContainerProxy(contextId: String) {
|
||||||
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
|
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
|
||||||
|
ResultConsumer<JSONObject> {
|
||||||
|
override fun success(result: JSONObject) {
|
||||||
|
val resultStatus = result.getBoolean("result")
|
||||||
|
callback(Result.success(resultStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||||
|
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+52
@@ -20,6 +20,10 @@ object ContainerProxyFeature {
|
|||||||
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
|
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
|
||||||
private const val CONTAINER_PROXY_REPORTER_MESSAGING_ID = "containerProxy"
|
private const val CONTAINER_PROXY_REPORTER_MESSAGING_ID = "containerProxy"
|
||||||
|
|
||||||
|
private var nextRequestId: Int = 0
|
||||||
|
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
// This is an internal var to make it mutable for unit testing purposes only
|
// This is an internal var to make it mutable for unit testing purposes only
|
||||||
internal var extensionController = WebExtensionController(
|
internal var extensionController = WebExtensionController(
|
||||||
@@ -40,7 +44,55 @@ object ContainerProxyFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun scheduleRequestWithResponse(
|
||||||
|
command: String,
|
||||||
|
args: Any,
|
||||||
|
callback: ResultConsumer<JSONObject>
|
||||||
|
) {
|
||||||
|
val message = JSONObject()
|
||||||
|
message.put("action", command);
|
||||||
|
message.put("args", args)
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
mutex.withLock {
|
||||||
|
message.put("id", nextRequestId)
|
||||||
|
|
||||||
|
requestHandlers[nextRequestId] = callback
|
||||||
|
|
||||||
|
nextRequestId += 1
|
||||||
|
|
||||||
|
extensionController.sendBackgroundMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
|
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
|
||||||
|
override fun onPortMessage(message: Any, port: Port) {
|
||||||
|
runBlocking {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
mutex.withLock {
|
||||||
|
val messageJSON = message as JSONObject;
|
||||||
|
val type = messageJSON.getString("type")
|
||||||
|
|
||||||
|
if (type == "healthcheck") {
|
||||||
|
val requestId = messageJSON.getInt("id")
|
||||||
|
val status = messageJSON.getString("status")
|
||||||
|
if (status == "success") {
|
||||||
|
requestHandlers[requestId]?.success(message)
|
||||||
|
} else {
|
||||||
|
requestHandlers[requestId]?.error(
|
||||||
|
"Container Proxy",
|
||||||
|
"Failed to perform operation",
|
||||||
|
message.getString("error")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
|
|||||||
) {
|
) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is ContentAction.UpdateThumbnailAction -> {
|
is ContentAction.UpdateThumbnailAction -> {
|
||||||
val resized = action.thumbnail.resize(maxWidth = 640, maxHeight = 480);
|
val resized = action.thumbnail.resize(maxWidth = 1280, maxHeight = 800);
|
||||||
val bytes = resized.toWebPBytes()
|
val bytes = resized.toWebPBytes()
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
|||||||
+36
@@ -1994,6 +1994,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
|||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface GeckoBrowserApi {
|
interface GeckoBrowserApi {
|
||||||
|
fun initialize()
|
||||||
fun showNativeFragment(): Boolean
|
fun showNativeFragment(): Boolean
|
||||||
fun onTrimMemory(level: Long)
|
fun onTrimMemory(level: Long)
|
||||||
|
|
||||||
@@ -2006,6 +2007,22 @@ interface GeckoBrowserApi {
|
|||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBrowserApi?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBrowserApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.initialize()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
@@ -3019,6 +3036,7 @@ interface GeckoContainerProxyApi {
|
|||||||
fun setProxyPort(port: Long)
|
fun setProxyPort(port: Long)
|
||||||
fun addContainerProxy(contextId: String)
|
fun addContainerProxy(contextId: String)
|
||||||
fun removeContainerProxy(contextId: String)
|
fun removeContainerProxy(contextId: String)
|
||||||
|
fun healthcheck(callback: (Result<Boolean>) -> Unit)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by GeckoContainerProxyApi. */
|
/** The codec used by GeckoContainerProxyApi. */
|
||||||
@@ -3083,6 +3101,24 @@ interface GeckoContainerProxyApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.healthcheck{ result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -7,7 +7,8 @@ console.log('Background script started')
|
|||||||
const store = new Store()
|
const store = new Store()
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy';
|
id: String | undefined;
|
||||||
|
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck';
|
||||||
args: any;
|
args: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +36,14 @@ port.onMessage.addListener((raw: unknown): void => {
|
|||||||
case "removeContainerProxy":
|
case "removeContainerProxy":
|
||||||
store.removeContainerProxyRelation(message.args, "tor")
|
store.removeContainerProxyRelation(message.args, "tor")
|
||||||
break
|
break
|
||||||
|
case "healthcheck":
|
||||||
|
port.postMessage({
|
||||||
|
"type": "healthcheck",
|
||||||
|
"id": message.id,
|
||||||
|
"status": "success",
|
||||||
|
"result": true
|
||||||
|
});
|
||||||
|
break
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export 'src/data/models/load_url_flags.dart';
|
|||||||
export 'src/data/models/source.dart';
|
export 'src/data/models/source.dart';
|
||||||
export 'src/domain/entities/default_selection_actions.dart';
|
export 'src/domain/entities/default_selection_actions.dart';
|
||||||
export 'src/domain/services/gecko_addon.dart';
|
export 'src/domain/services/gecko_addon.dart';
|
||||||
|
export 'src/domain/services/gecko_browser.dart';
|
||||||
export 'src/domain/services/gecko_browser_extension.dart';
|
export 'src/domain/services/gecko_browser_extension.dart';
|
||||||
export 'src/domain/services/gecko_container_proxy.dart';
|
export 'src/domain/services/gecko_container_proxy.dart';
|
||||||
export 'src/domain/services/gecko_cookie.dart';
|
export 'src/domain/services/gecko_cookie.dart';
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ class GeckoBrowserService {
|
|||||||
|
|
||||||
GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? _apiInstance;
|
GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? _apiInstance;
|
||||||
|
|
||||||
|
Future<void> initialize() {
|
||||||
|
return _api.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> showNativeFragment() {
|
Future<bool> showNativeFragment() {
|
||||||
return _api.showNativeFragment();
|
return _api.showNativeFragment();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,12 @@ class GeckoContainerProxyService {
|
|||||||
Future<void> removeContainerProxy(String contextId) {
|
Future<void> removeContainerProxy(String contextId) {
|
||||||
return _apiInstance.removeContainerProxy(contextId);
|
return _apiInstance.removeContainerProxy(contextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> healthcheck() {
|
||||||
|
try {
|
||||||
|
return _apiInstance.healthcheck();
|
||||||
|
} catch (_) {
|
||||||
|
return Future.value(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1983,6 +1983,29 @@ class GeckoBrowserApi {
|
|||||||
|
|
||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> showNativeFragment() async {
|
Future<bool> showNativeFragment() async {
|
||||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$pigeonVar_messageChannelSuffix';
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$pigeonVar_messageChannelSuffix';
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
@@ -3251,6 +3274,34 @@ class GeckoContainerProxyApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> healthcheck() async {
|
||||||
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as bool?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeckoCookieApi {
|
class GeckoCookieApi {
|
||||||
|
|||||||
@@ -736,6 +736,7 @@ class ShareInternetResourceState {
|
|||||||
)
|
)
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class GeckoBrowserApi {
|
abstract class GeckoBrowserApi {
|
||||||
|
void initialize();
|
||||||
bool showNativeFragment();
|
bool showNativeFragment();
|
||||||
void onTrimMemory(int level);
|
void onTrimMemory(int level);
|
||||||
}
|
}
|
||||||
@@ -953,6 +954,9 @@ abstract class GeckoContainerProxyApi {
|
|||||||
void setProxyPort(int port);
|
void setProxyPort(int port);
|
||||||
void addContainerProxy(String contextId);
|
void addContainerProxy(String contextId);
|
||||||
void removeContainerProxy(String contextId);
|
void removeContainerProxy(String contextId);
|
||||||
|
|
||||||
|
@async
|
||||||
|
bool healthcheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
|
|||||||
Reference in New Issue
Block a user