intermediate
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|||||||
-3
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
@@ -1,39 +0,0 @@
|
|||||||
import { Readability, isProbablyReaderable } from '@mozilla/readability';
|
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
function isReaderable() {
|
|
||||||
return isProbablyReaderable(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseReaderable(doc) {
|
|
||||||
const reader = new Readability(doc);
|
|
||||||
const article = reader.parse();
|
|
||||||
return article;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReaderable() {
|
|
||||||
const clonedDoc = document.cloneNode(true);
|
|
||||||
const article = parseReaderable(clonedDoc);
|
|
||||||
const markup = DOMPurify.sanitize(article.content);
|
|
||||||
|
|
||||||
// console.log('article title: ', parsed.title);
|
|
||||||
// console.log('HTML string of processed article content: ', parsed.content);
|
|
||||||
// console.log('text content of the article, with all the HTML tags removed: ', parsed.textContent);
|
|
||||||
// console.log('length of an article, in characters: ', parsed.length);
|
|
||||||
// console.log('article description, or short excerpt from the content: ', parsed.excerpt);
|
|
||||||
// console.log('author metadata: ', parsed.byline);
|
|
||||||
// console.log('content direction: ', parsed.dir);
|
|
||||||
// console.log('name of the site: ', parsed.siteName);
|
|
||||||
// console.log('content language: ', parsed.lang);
|
|
||||||
// console.log('published time: ', parsed.publishedTime);
|
|
||||||
|
|
||||||
if (markup) {
|
|
||||||
document.body.innerHTML = '<div style="margin: 4pt;">' + markup + '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach functions to the window object
|
|
||||||
window.isReaderable = isReaderable;
|
|
||||||
window.applyReaderable = applyReaderable;
|
|
||||||
|
|
||||||
export { isReaderable, applyReaderable };
|
|
||||||
@@ -8,8 +8,9 @@ import 'package:lensai/features/bangs/presentation/screens/search.dart';
|
|||||||
import 'package:lensai/features/chat_archive/presentation/screens/detail.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/detail.dart';
|
||||||
import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
|
||||||
import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
|
import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/screens/browser.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/screens/search.dart';
|
||||||
import 'package:lensai/features/settings/presentation/screens/settings.dart';
|
import 'package:lensai/features/settings/presentation/screens/settings.dart';
|
||||||
|
|
||||||
part 'routes.g.dart';
|
part 'routes.g.dart';
|
||||||
@@ -22,13 +23,17 @@ part 'routes.g.dart';
|
|||||||
name: 'AboutRoute',
|
name: 'AboutRoute',
|
||||||
path: 'about',
|
path: 'about',
|
||||||
),
|
),
|
||||||
|
TypedGoRoute<SearchRoute>(
|
||||||
|
name: 'SearchRoute',
|
||||||
|
path: 'search/:searchText',
|
||||||
|
),
|
||||||
TypedGoRoute<BangCategoriesRoute>(
|
TypedGoRoute<BangCategoriesRoute>(
|
||||||
name: 'BangRoute',
|
name: 'BangRoute',
|
||||||
path: 'bangs',
|
path: 'bangs',
|
||||||
routes: [
|
routes: [
|
||||||
TypedGoRoute<BangSearchRoute>(
|
TypedGoRoute<BangSearchRoute>(
|
||||||
name: 'BangSearchRoute',
|
name: 'BangSearchRoute',
|
||||||
path: 'search',
|
path: 'search/:searchText',
|
||||||
),
|
),
|
||||||
TypedGoRoute<BangCategoryRoute>(
|
TypedGoRoute<BangCategoryRoute>(
|
||||||
name: 'BangCategoryRoute',
|
name: 'BangCategoryRoute',
|
||||||
@@ -62,6 +67,24 @@ class AboutRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SearchRoute extends GoRouteData {
|
||||||
|
static const String emptySearchText = ' ';
|
||||||
|
|
||||||
|
//This should be nullable but isnt allowed by go_router
|
||||||
|
final String searchText;
|
||||||
|
|
||||||
|
const SearchRoute({this.searchText = SearchRoute.emptySearchText});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return SearchScreen(
|
||||||
|
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||||
|
? null
|
||||||
|
: searchText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class BangCategoriesRoute extends GoRouteData {
|
class BangCategoriesRoute extends GoRouteData {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
@@ -96,9 +119,20 @@ class BangSubCategoryRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BangSearchRoute extends GoRouteData {
|
class BangSearchRoute extends GoRouteData {
|
||||||
|
static const String emptySearchText = ' ';
|
||||||
|
|
||||||
|
//This should be nullable but isnt allowed by go_router
|
||||||
|
final String searchText;
|
||||||
|
|
||||||
|
const BangSearchRoute({this.searchText = BangSearchRoute.emptySearchText});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
return const BangSearchScreen();
|
return BangSearchScreen(
|
||||||
|
initialSearchText: (searchText.isEmpty || searchText == emptySearchText)
|
||||||
|
? null
|
||||||
|
: searchText,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ RouteBase get $browserRoute => GoRouteData.$route(
|
|||||||
name: 'AboutRoute',
|
name: 'AboutRoute',
|
||||||
factory: $AboutRouteExtension._fromState,
|
factory: $AboutRouteExtension._fromState,
|
||||||
),
|
),
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'search/:searchText',
|
||||||
|
name: 'SearchRoute',
|
||||||
|
factory: $SearchRouteExtension._fromState,
|
||||||
|
),
|
||||||
GoRouteData.$route(
|
GoRouteData.$route(
|
||||||
path: 'bangs',
|
path: 'bangs',
|
||||||
name: 'BangRoute',
|
name: 'BangRoute',
|
||||||
factory: $BangCategoriesRouteExtension._fromState,
|
factory: $BangCategoriesRouteExtension._fromState,
|
||||||
routes: [
|
routes: [
|
||||||
GoRouteData.$route(
|
GoRouteData.$route(
|
||||||
path: 'search',
|
path: 'search/:searchText',
|
||||||
name: 'BangSearchRoute',
|
name: 'BangSearchRoute',
|
||||||
factory: $BangSearchRouteExtension._fromState,
|
factory: $BangSearchRouteExtension._fromState,
|
||||||
),
|
),
|
||||||
@@ -88,6 +93,26 @@ extension $AboutRouteExtension on AboutRoute {
|
|||||||
void replace(BuildContext context) => context.replace(location);
|
void replace(BuildContext context) => context.replace(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension $SearchRouteExtension on SearchRoute {
|
||||||
|
static SearchRoute _fromState(GoRouterState state) => SearchRoute(
|
||||||
|
searchText:
|
||||||
|
state.pathParameters['searchText']! ?? SearchRoute.emptySearchText,
|
||||||
|
);
|
||||||
|
|
||||||
|
String get location => GoRouteData.$location(
|
||||||
|
'/search/${Uri.encodeComponent(searchText)}',
|
||||||
|
);
|
||||||
|
|
||||||
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|
||||||
|
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||||
|
|
||||||
|
void pushReplacement(BuildContext context) =>
|
||||||
|
context.pushReplacement(location);
|
||||||
|
|
||||||
|
void replace(BuildContext context) => context.replace(location);
|
||||||
|
}
|
||||||
|
|
||||||
extension $BangCategoriesRouteExtension on BangCategoriesRoute {
|
extension $BangCategoriesRouteExtension on BangCategoriesRoute {
|
||||||
static BangCategoriesRoute _fromState(GoRouterState state) =>
|
static BangCategoriesRoute _fromState(GoRouterState state) =>
|
||||||
BangCategoriesRoute();
|
BangCategoriesRoute();
|
||||||
@@ -107,10 +132,13 @@ extension $BangCategoriesRouteExtension on BangCategoriesRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension $BangSearchRouteExtension on BangSearchRoute {
|
extension $BangSearchRouteExtension on BangSearchRoute {
|
||||||
static BangSearchRoute _fromState(GoRouterState state) => BangSearchRoute();
|
static BangSearchRoute _fromState(GoRouterState state) => BangSearchRoute(
|
||||||
|
searchText: state.pathParameters['searchText']! ??
|
||||||
|
BangSearchRoute.emptySearchText,
|
||||||
|
);
|
||||||
|
|
||||||
String get location => GoRouteData.$location(
|
String get location => GoRouteData.$location(
|
||||||
'/bangs/search',
|
'/bangs/search/${Uri.encodeComponent(searchText)}',
|
||||||
);
|
);
|
||||||
|
|
||||||
void go(BuildContext context) => context.go(location);
|
void go(BuildContext context) => context.go(location);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:lensai/core/http_error_handler.dart';
|
import 'package:lensai/core/http_error_handler.dart';
|
||||||
import 'package:lensai/data/models/web_page_info.dart';
|
import 'package:lensai/data/models/web_page_info.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
|
||||||
|
import 'package:lensai/utils/lru_cache.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:universal_io/io.dart';
|
|
||||||
|
|
||||||
part 'generic_website.g.dart';
|
part 'generic_website.g.dart';
|
||||||
|
|
||||||
@@ -56,11 +56,11 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
|
|
||||||
late http.Client _client;
|
late http.Client _client;
|
||||||
|
|
||||||
final Map<String, bool> _httpsCache;
|
final LRUCache<String, BrowserIcon> _iconCache;
|
||||||
|
|
||||||
GenericWebsiteService()
|
GenericWebsiteService()
|
||||||
: _httpsCache = {},
|
: _iconsService = GeckoIconService(),
|
||||||
_iconsService = GeckoIconService();
|
_iconCache = LRUCache(50);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
@@ -207,49 +207,59 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
required Uri url,
|
required Uri url,
|
||||||
List<Resource> resources = const [],
|
List<Resource> resources = const [],
|
||||||
}) async {
|
}) async {
|
||||||
final result = await _iconsService.loadIcon(url: url, resources: resources);
|
final cached = _iconCache.get(url.host);
|
||||||
return BrowserIcon.fromBytes(
|
if (cached == null) {
|
||||||
result.image,
|
final result =
|
||||||
dominantColor: (result.color != null) ? Color(result.color!) : null,
|
await _iconsService.loadIcon(url: url, resources: resources);
|
||||||
source: result.source,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
|
return _iconCache.set(
|
||||||
if (httpUri.isScheme('https')) {
|
url.host,
|
||||||
return httpUri;
|
await BrowserIcon.fromBytes(
|
||||||
} else if (httpUri.isScheme('http')) {
|
result.image,
|
||||||
final cached = _httpsCache[httpUri.host];
|
dominantColor: (result.color != null) ? Color(result.color!) : null,
|
||||||
if (cached != null) {
|
source: result.source,
|
||||||
return cached ? httpUri.replace(scheme: 'https') : null;
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
var sslAvailable = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final context = SecurityContext.defaultContext;
|
|
||||||
|
|
||||||
final socket = await SecureSocket.connect(
|
|
||||||
httpUri.host,
|
|
||||||
443,
|
|
||||||
context: context,
|
|
||||||
timeout: const Duration(seconds: 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
await socket.close();
|
|
||||||
|
|
||||||
sslAvailable = true;
|
|
||||||
} catch (_) {
|
|
||||||
sslAvailable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_httpsCache[httpUri.host] = sslAvailable;
|
|
||||||
|
|
||||||
if (sslAvailable) {
|
|
||||||
return httpUri.replace(scheme: 'https');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Future<Uri?> tryUpgradeToHttps(Uri httpUri) async {
|
||||||
|
// if (httpUri.isScheme('https')) {
|
||||||
|
// return httpUri;
|
||||||
|
// } else if (httpUri.isScheme('http')) {
|
||||||
|
// final cached = _httpsCache[httpUri.host];
|
||||||
|
// if (cached != null) {
|
||||||
|
// return cached ? httpUri.replace(scheme: 'https') : null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var sslAvailable = false;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// final context = SecurityContext.defaultContext;
|
||||||
|
|
||||||
|
// final socket = await SecureSocket.connect(
|
||||||
|
// httpUri.host,
|
||||||
|
// 443,
|
||||||
|
// context: context,
|
||||||
|
// timeout: const Duration(seconds: 3),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// await socket.close();
|
||||||
|
|
||||||
|
// sslAvailable = true;
|
||||||
|
// } catch (_) {
|
||||||
|
// sslAvailable = false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// _httpsCache[httpUri.host] = sslAvailable;
|
||||||
|
|
||||||
|
// if (sslAvailable) {
|
||||||
|
// return httpUri.replace(scheme: 'https');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'0b1d953fe1b30b9dd0396df34e6176424cdaf941';
|
r'7dea42d6914b0f06250a96bb425ba10a331ef0d3';
|
||||||
|
|
||||||
/// See also [GenericWebsiteService].
|
/// See also [GenericWebsiteService].
|
||||||
@ProviderFor(GenericWebsiteService)
|
@ProviderFor(GenericWebsiteService)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Selectable<BangData> queryBangs(String searchString) {
|
Selectable<BangData> queryBangs(String searchString) {
|
||||||
return db.bangQuery(query: db.buildQuery(searchString));
|
return db.bangQuery(query: db.buildFtsQuery(searchString));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<int> upsertBangIcon(String trigger, BrowserIcon icon) async {
|
// Future<int> upsertBangIcon(String trigger, BrowserIcon icon) async {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:lensai/features/bangs/data/database/daos/sync.dart';
|
|||||||
import 'package:lensai/features/bangs/data/database/drift/converters/bang_format.dart';
|
import 'package:lensai/features/bangs/data/database/drift/converters/bang_format.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang.dart';
|
import 'package:lensai/features/bangs/data/models/bang.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/query/domain/tokenizer.dart';
|
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ import 'package:lensai/features/bangs/domain/repositories/sync.dart';
|
|||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'providers.g.dart';
|
part 'bangs.g.dart';
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<BangData?> bangData(Ref ref, String? trigger) {
|
Stream<BangData?> bangData(Ref ref, String? trigger) {
|
||||||
@@ -14,7 +14,7 @@ Stream<BangData?> bangData(Ref ref, String? trigger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Stream<BangData?> kagiSearchBangData(Ref ref) {
|
Stream<BangData?> defaultSearchBangData(Ref ref) {
|
||||||
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
final repository = ref.watch(bangDataRepositoryProvider.notifier);
|
||||||
return repository.watchBang('kagi');
|
return repository.watchBang('kagi');
|
||||||
}
|
}
|
||||||
+10
-10
@@ -1,6 +1,6 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'providers.dart';
|
part of 'bangs.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
@@ -156,24 +156,24 @@ class _BangDataProviderElement
|
|||||||
String? get trigger => (origin as BangDataProvider).trigger;
|
String? get trigger => (origin as BangDataProvider).trigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$kagiSearchBangDataHash() =>
|
String _$defaultSearchBangDataHash() =>
|
||||||
r'0bb5014a7af73ef6f5a36b0545d7a73e7ad0580a';
|
r'b7547084b261ccb1dc02a6c7230fe57f6902dd58';
|
||||||
|
|
||||||
/// See also [kagiSearchBangData].
|
/// See also [defaultSearchBangData].
|
||||||
@ProviderFor(kagiSearchBangData)
|
@ProviderFor(defaultSearchBangData)
|
||||||
final kagiSearchBangDataProvider = StreamProvider<BangData?>.internal(
|
final defaultSearchBangDataProvider = StreamProvider<BangData?>.internal(
|
||||||
kagiSearchBangData,
|
defaultSearchBangData,
|
||||||
name: r'kagiSearchBangDataProvider',
|
name: r'defaultSearchBangDataProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$kagiSearchBangDataHash,
|
: _$defaultSearchBangDataHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@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 KagiSearchBangDataRef = StreamProviderRef<BangData?>;
|
typedef DefaultSearchBangDataRef = StreamProviderRef<BangData?>;
|
||||||
String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a';
|
String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a';
|
||||||
|
|
||||||
/// See also [bangCategories].
|
/// See also [bangCategories].
|
||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:lensai/features/bangs/data/database/database.dart';
|
import 'package:lensai/features/bangs/data/database/database.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/data/providers.dart';
|
import 'package:lensai/features/bangs/data/providers.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'search.g.dart';
|
part 'search.g.dart';
|
||||||
@@ -30,3 +31,29 @@ class BangSearch extends _$BangSearch {
|
|||||||
return _streamController.stream;
|
return _streamController.stream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class SeamlessBangProvider extends _$SeamlessBangProvider {
|
||||||
|
bool _hasSearch = false;
|
||||||
|
|
||||||
|
void search(String input) {
|
||||||
|
if (input.isNotEmpty) {
|
||||||
|
if (!_hasSearch) {
|
||||||
|
_hasSearch = true;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
unawaited(ref.read(bangSearchProvider.notifier).search(input));
|
||||||
|
} else if (_hasSearch) {
|
||||||
|
_hasSearch = false;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AsyncValue<List<BangData>> build() {
|
||||||
|
return _hasSearch
|
||||||
|
? ref.watch(bangSearchProvider)
|
||||||
|
: ref.watch(frequentBangDataListProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,5 +21,23 @@ final bangSearchProvider =
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$BangSearch = AutoDisposeStreamNotifier<List<BangData>>;
|
typedef _$BangSearch = AutoDisposeStreamNotifier<List<BangData>>;
|
||||||
|
String _$seamlessBangProviderHash() =>
|
||||||
|
r'd2c8e94d6305a6a37cc27f8bc3617521f513e537';
|
||||||
|
|
||||||
|
/// See also [SeamlessBangProvider].
|
||||||
|
@ProviderFor(SeamlessBangProvider)
|
||||||
|
final seamlessBangProviderProvider = AutoDisposeNotifierProvider<
|
||||||
|
SeamlessBangProvider, AsyncValue<List<BangData>>>.internal(
|
||||||
|
SeamlessBangProvider.new,
|
||||||
|
name: r'seamlessBangProviderProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$seamlessBangProviderHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$SeamlessBangProvider
|
||||||
|
= AutoDisposeNotifier<AsyncValue<List<BangData>>>;
|
||||||
// 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
|
||||||
|
|||||||
@@ -4,7 +4,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/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
|
|
||||||
class BangCategoriesScreen extends HookConsumerWidget {
|
class BangCategoriesScreen extends HookConsumerWidget {
|
||||||
@@ -19,7 +19,7 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await context.push(BangSearchRoute().location);
|
await context.push(const BangSearchRoute().location);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.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/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ 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';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/routing/routes.dart';
|
|
||||||
import 'package:lensai/features/bangs/domain/repositories/search.dart';
|
import 'package:lensai/features/bangs/domain/repositories/search.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
|
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||||
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
|
|
||||||
class BangSearchScreen extends HookConsumerWidget {
|
class BangSearchScreen extends HookConsumerWidget {
|
||||||
const BangSearchScreen({super.key});
|
final String? initialSearchText;
|
||||||
|
|
||||||
|
const BangSearchScreen({super.key, this.initialSearchText});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -29,7 +27,8 @@ class BangSearchScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final textEditingController = useTextEditingController();
|
final textEditingController =
|
||||||
|
useTextEditingController(text: initialSearchText);
|
||||||
|
|
||||||
useListenableCallback(textEditingController, () async {
|
useListenableCallback(textEditingController, () async {
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -78,14 +77,15 @@ class BangSearchScreen extends HookConsumerWidget {
|
|||||||
.read(selectedBangTriggerProvider().notifier)
|
.read(selectedBangTriggerProvider().notifier)
|
||||||
.setTrigger(bang.trigger);
|
.setTrigger(bang.trigger);
|
||||||
|
|
||||||
if (ref.read(bottomSheetControllerProvider)
|
// if (ref.read(bottomSheetControllerProvider)
|
||||||
is! CreateTabSheet) {
|
// is! CreateTabSheet) {
|
||||||
ref.read(bottomSheetControllerProvider.notifier).show(
|
// ref.read(bottomSheetControllerProvider.notifier).show(
|
||||||
CreateTabSheet(preferredTool: KagiTool.search),
|
// CreateTabSheet(preferredTool: KagiTool.search),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
context.go(BrowserRoute().location);
|
// context.go(BrowserRoute().location);
|
||||||
|
context.pop();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
class BangIcon extends HookConsumerWidget {
|
class BangIcon extends HookConsumerWidget {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
|
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
||||||
import 'package:lensai/features/kagi/domain/repositories/autosuggest.dart';
|
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||||
import 'package:lensai/presentation/widgets/autocomplete.dart';
|
import 'package:lensai/presentation/widgets/autocomplete.dart';
|
||||||
@@ -36,7 +36,7 @@ class SearchField extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final optionsStream = ref.watch(autosuggestRepositoryProvider);
|
final optionsStream = ref.watch(searchSuggestionsProvider());
|
||||||
|
|
||||||
final focusNode = useFocusNode();
|
final focusNode = useFocusNode();
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class SearchField extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
onTextChanged: (textEditingValue) {
|
onTextChanged: (textEditingValue) {
|
||||||
ref
|
ref
|
||||||
.read(autosuggestRepositoryProvider.notifier)
|
.read(searchSuggestionsProvider().notifier)
|
||||||
.addQuery(textEditingValue.text);
|
.addQuery(textEditingValue.text);
|
||||||
},
|
},
|
||||||
fieldViewBuilder: (
|
fieldViewBuilder: (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class SearchDao extends DatabaseAccessor<ChatSearchDatabase>
|
|||||||
required String searchString,
|
required String searchString,
|
||||||
}) {
|
}) {
|
||||||
return db.chatQuery(
|
return db.chatQuery(
|
||||||
query: db.buildQuery(searchString),
|
query: db.buildFtsQuery(searchString),
|
||||||
snippetLength: snippetLength,
|
snippetLength: snippetLength,
|
||||||
beforeMatch: matchPrefix,
|
beforeMatch: matchPrefix,
|
||||||
afterMatch: matchSuffix,
|
afterMatch: matchSuffix,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:lensai/features/chat_archive/data/database/daos/search.dart';
|
import 'package:lensai/features/chat_archive/data/database/daos/search.dart';
|
||||||
import 'package:lensai/features/query/domain/tokenizer.dart';
|
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ CREATE TRIGGER chat_after_update AFTER UPDATE ON chat BEGIN
|
|||||||
chat_fts(chat_fts, rowid, title, content)
|
chat_fts(chat_fts, rowid, title, content)
|
||||||
VALUES('delete', old.rowid, old.title, old.content);
|
VALUES('delete', old.rowid, old.title, old.content);
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
chat_fts(chat_fts, title, content)
|
chat_fts(rowid, title, content)
|
||||||
VALUES (new.rowid, new.title, new.content);
|
VALUES (new.rowid, new.title, new.content);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ abstract class _$ChatSearchDatabase extends GeneratedDatabase {
|
|||||||
'CREATE TRIGGER chat_after_delete AFTER DELETE ON chat BEGIN INSERT INTO chat_fts (chat_fts, "rowid", title, content) VALUES (\'delete\', old."rowid", old.title, old.content);END',
|
'CREATE TRIGGER chat_after_delete AFTER DELETE ON chat BEGIN INSERT INTO chat_fts (chat_fts, "rowid", title, content) VALUES (\'delete\', old."rowid", old.title, old.content);END',
|
||||||
'chat_after_delete');
|
'chat_after_delete');
|
||||||
late final Trigger chatAfterUpdate = Trigger(
|
late final Trigger chatAfterUpdate = Trigger(
|
||||||
'CREATE TRIGGER chat_after_update AFTER UPDATE ON chat BEGIN INSERT INTO chat_fts (chat_fts, "rowid", title, content) VALUES (\'delete\', old."rowid", old.title, old.content);INSERT INTO chat_fts (chat_fts, title, content) VALUES (new."rowid", new.title, new.content);END',
|
'CREATE TRIGGER chat_after_update AFTER UPDATE ON chat BEGIN INSERT INTO chat_fts (chat_fts, "rowid", title, content) VALUES (\'delete\', old."rowid", old.title, old.content);INSERT INTO chat_fts ("rowid", title, content) VALUES (new."rowid", new.title, new.content);END',
|
||||||
'chat_after_update');
|
'chat_after_update');
|
||||||
late final SearchDao searchDao = SearchDao(this as ChatSearchDatabase);
|
late final SearchDao searchDao = SearchDao(this as ChatSearchDatabase);
|
||||||
Selectable<ChatQueryResult> chatQuery(
|
Selectable<ChatQueryResult> chatQuery(
|
||||||
|
|||||||
@@ -83,10 +83,6 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
|
|||||||
_streamController = StreamController();
|
_streamController = StreamController();
|
||||||
_searchDatabase = ref.watch(chatSearchDatabaseProvider);
|
_searchDatabase = ref.watch(chatSearchDatabaseProvider);
|
||||||
|
|
||||||
ref.onDispose(() async {
|
|
||||||
await _streamController.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// populate with initial chats
|
// populate with initial chats
|
||||||
await _searchDatabase.searchDao.deleteAllChats();
|
await _searchDatabase.searchDao.deleteAllChats();
|
||||||
await _searchDatabase.searchDao.indexChats(await _availableChats());
|
await _searchDatabase.searchDao.indexChats(await _availableChats());
|
||||||
@@ -112,6 +108,7 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
|
|||||||
|
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
await changeStreamSubscription.cancel();
|
await changeStreamSubscription.cancel();
|
||||||
|
await _streamController.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
yield* _streamController.stream;
|
yield* _streamController.stream;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'search.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$chatArchiveSearchRepositoryHash() =>
|
String _$chatArchiveSearchRepositoryHash() =>
|
||||||
r'a3decc7975bf49995b20ea578db3c08b69f7c03c';
|
r'd7174cfb877c61ee4e9580f44d7ffd11bf3b4b60';
|
||||||
|
|
||||||
/// See also [ChatArchiveSearchRepository].
|
/// See also [ChatArchiveSearchRepository].
|
||||||
@ProviderFor(ChatArchiveSearchRepository)
|
@ProviderFor(ChatArchiveSearchRepository)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
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/features/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
@@ -22,7 +22,7 @@ GeckoSelectionActionService selectionActionService(
|
|||||||
service.setActions([
|
service.setActions([
|
||||||
SearchAction((text) async {
|
SearchAction((text) async {
|
||||||
final defaultSearchBang =
|
final defaultSearchBang =
|
||||||
await ref.read(kagiSearchBangDataProvider.future);
|
await ref.read(defaultSearchBangDataProvider.future);
|
||||||
|
|
||||||
if (defaultSearchBang != null) {
|
if (defaultSearchBang != null) {
|
||||||
await ref
|
await ref
|
||||||
@@ -34,7 +34,7 @@ GeckoSelectionActionService selectionActionService(
|
|||||||
}),
|
}),
|
||||||
PrivateSearchAction((text) async {
|
PrivateSearchAction((text) async {
|
||||||
final defaultSearchBang =
|
final defaultSearchBang =
|
||||||
await ref.read(kagiSearchBangDataProvider.future);
|
await ref.read(defaultSearchBangDataProvider.future);
|
||||||
|
|
||||||
if (defaultSearchBang != null) {
|
if (defaultSearchBang != null) {
|
||||||
await ref.read(tabRepositoryProvider.notifier).addTab(
|
await ref.read(tabRepositoryProvider.notifier).addTab(
|
||||||
@@ -96,6 +96,17 @@ GeckoAddonService addonService(Ref ref) {
|
|||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
GeckoTabContentService tabContentService(Ref ref) {
|
||||||
|
final service = GeckoTabContentService.setUp();
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
service.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class EngineReadyState extends _$EngineReadyState {
|
class EngineReadyState extends _$EngineReadyState {
|
||||||
@override
|
@override
|
||||||
@@ -133,5 +144,5 @@ class EngineReadyState extends _$EngineReadyState {
|
|||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Raw<TabSession> selectedTabSessionNotifier(Ref ref) {
|
Raw<TabSession> selectedTabSessionNotifier(Ref ref) {
|
||||||
return ref.watch(tabSessionProvider(null).notifier);
|
return ref.watch(tabSessionProvider(tabId: null).notifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'providers.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$selectionActionServiceHash() =>
|
String _$selectionActionServiceHash() =>
|
||||||
r'b5bc6b2bc5496ae84d61a74cc6ff052b55d2a3d3';
|
r'd0c04262171976c21441297706273417e3d79b67';
|
||||||
|
|
||||||
/// See also [selectionActionService].
|
/// See also [selectionActionService].
|
||||||
@ProviderFor(selectionActionService)
|
@ProviderFor(selectionActionService)
|
||||||
@@ -57,8 +57,25 @@ final addonServiceProvider = Provider<GeckoAddonService>.internal(
|
|||||||
@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 AddonServiceRef = ProviderRef<GeckoAddonService>;
|
typedef AddonServiceRef = ProviderRef<GeckoAddonService>;
|
||||||
|
String _$tabContentServiceHash() => r'd9a991add907ecc138c62790883e59d8e9aa9266';
|
||||||
|
|
||||||
|
/// See also [tabContentService].
|
||||||
|
@ProviderFor(tabContentService)
|
||||||
|
final tabContentServiceProvider = Provider<GeckoTabContentService>.internal(
|
||||||
|
tabContentService,
|
||||||
|
name: r'tabContentServiceProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$tabContentServiceHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef TabContentServiceRef = ProviderRef<GeckoTabContentService>;
|
||||||
String _$selectedTabSessionNotifierHash() =>
|
String _$selectedTabSessionNotifierHash() =>
|
||||||
r'62fcd98a915566d55b5bcb297d87d675fe12786d';
|
r'aef19991d5b05a2bb28e8a6ccb57dc75c2f3148f';
|
||||||
|
|
||||||
/// See also [selectedTabSessionNotifier].
|
/// See also [selectedTabSessionNotifier].
|
||||||
@ProviderFor(selectedTabSessionNotifier)
|
@ProviderFor(selectedTabSessionNotifier)
|
||||||
|
|||||||
@@ -2,22 +2,16 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'selected_tab.g.dart';
|
part 'selected_tab.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class SelectedTab extends _$SelectedTab {
|
class SelectedTab extends _$SelectedTab {
|
||||||
late TabDatabase _db;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? build() {
|
String? build() {
|
||||||
final eventSerivce = ref.watch(eventServiceProvider);
|
final eventSerivce = ref.watch(eventServiceProvider);
|
||||||
|
|
||||||
_db = ref.watch(tabDatabaseProvider);
|
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
engineReadyStateProvider,
|
engineReadyStateProvider,
|
||||||
@@ -31,10 +25,6 @@ class SelectedTab extends _$SelectedTab {
|
|||||||
final selectedTabSub = eventSerivce.selectedTabEvents.listen(
|
final selectedTabSub = eventSerivce.selectedTabEvents.listen(
|
||||||
(tabId) async {
|
(tabId) async {
|
||||||
state = tabId;
|
state = tabId;
|
||||||
|
|
||||||
if (tabId != null) {
|
|
||||||
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'selected_tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$selectedTabHash() => r'e49fb1c3938a97f384673e158742e56479aa8a8f';
|
String _$selectedTabHash() => r'f550ac5b471bab8f8e5d75ef4d7669cde769db9a';
|
||||||
|
|
||||||
/// See also [SelectedTab].
|
/// See also [SelectedTab].
|
||||||
@ProviderFor(SelectedTab)
|
@ProviderFor(SelectedTab)
|
||||||
|
|||||||
@@ -3,22 +3,16 @@ import 'dart:async';
|
|||||||
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/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'tab_list.g.dart';
|
part 'tab_list.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class TabList extends _$TabList {
|
class TabList extends _$TabList {
|
||||||
late TabDatabase _db;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> build() {
|
List<String> build() {
|
||||||
final eventService = ref.watch(eventServiceProvider);
|
final eventService = ref.watch(eventServiceProvider);
|
||||||
|
|
||||||
_db = ref.watch(tabDatabaseProvider);
|
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
engineReadyStateProvider,
|
engineReadyStateProvider,
|
||||||
@@ -32,15 +26,7 @@ class TabList extends _$TabList {
|
|||||||
final tabListSub = eventService.tabListEvents.listen(
|
final tabListSub = eventService.tabListEvents.listen(
|
||||||
(tabs) async {
|
(tabs) async {
|
||||||
if (!const DeepCollectionEquality().equals(state, tabs)) {
|
if (!const DeepCollectionEquality().equals(state, tabs)) {
|
||||||
//Make sure we only syncing empty lists when there has been tabs
|
|
||||||
//before as security measurement
|
|
||||||
final syncTabs = tabs.isNotEmpty || state.isNotEmpty;
|
|
||||||
|
|
||||||
state = tabs;
|
state = tabs;
|
||||||
|
|
||||||
if (syncTabs) {
|
|
||||||
await _db.tabDao.syncTabs(retainTabIds: tabs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabListHash() => r'3c17372cc467736ce1cfba937999acf4de7345ef';
|
String _$tabListHash() => r'039099bfa102a86fd73c0544ea71f93f91e9d352';
|
||||||
|
|
||||||
/// See also [TabList].
|
/// See also [TabList].
|
||||||
@ProviderFor(TabList)
|
@ProviderFor(TabList)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TabSession extends _$TabSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build(String? tabId) {
|
void build({required String? tabId}) {
|
||||||
_sessionService = (tabId != null)
|
_sessionService = (tabId != null)
|
||||||
? GeckoSessionService(tabId: tabId)
|
? GeckoSessionService(tabId: tabId)
|
||||||
: GeckoSessionService.forActiveTab();
|
: GeckoSessionService.forActiveTab();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab_session.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabSessionHash() => r'42b44a4d47b9a551d002fdbc6337c14b2c327dde';
|
String _$tabSessionHash() => r'e4fbbcd430037cc01adb799783fb7974a11a27bf';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -32,9 +32,9 @@ class _SystemHash {
|
|||||||
abstract class _$TabSession extends BuildlessAutoDisposeNotifier<void> {
|
abstract class _$TabSession extends BuildlessAutoDisposeNotifier<void> {
|
||||||
late final String? tabId;
|
late final String? tabId;
|
||||||
|
|
||||||
void build(
|
void build({
|
||||||
String? tabId,
|
required String? tabId,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See also [TabSession].
|
/// See also [TabSession].
|
||||||
@@ -47,11 +47,11 @@ class TabSessionFamily extends Family<void> {
|
|||||||
const TabSessionFamily();
|
const TabSessionFamily();
|
||||||
|
|
||||||
/// See also [TabSession].
|
/// See also [TabSession].
|
||||||
TabSessionProvider call(
|
TabSessionProvider call({
|
||||||
String? tabId,
|
required String? tabId,
|
||||||
) {
|
}) {
|
||||||
return TabSessionProvider(
|
return TabSessionProvider(
|
||||||
tabId,
|
tabId: tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class TabSessionFamily extends Family<void> {
|
|||||||
covariant TabSessionProvider provider,
|
covariant TabSessionProvider provider,
|
||||||
) {
|
) {
|
||||||
return call(
|
return call(
|
||||||
provider.tabId,
|
tabId: provider.tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +83,9 @@ class TabSessionFamily extends Family<void> {
|
|||||||
class TabSessionProvider
|
class TabSessionProvider
|
||||||
extends AutoDisposeNotifierProviderImpl<TabSession, void> {
|
extends AutoDisposeNotifierProviderImpl<TabSession, void> {
|
||||||
/// See also [TabSession].
|
/// See also [TabSession].
|
||||||
TabSessionProvider(
|
TabSessionProvider({
|
||||||
String? tabId,
|
required String? tabId,
|
||||||
) : this._internal(
|
}) : this._internal(
|
||||||
() => TabSession()..tabId = tabId,
|
() => TabSession()..tabId = tabId,
|
||||||
from: tabSessionProvider,
|
from: tabSessionProvider,
|
||||||
name: r'tabSessionProvider',
|
name: r'tabSessionProvider',
|
||||||
@@ -116,7 +116,7 @@ class TabSessionProvider
|
|||||||
covariant TabSession notifier,
|
covariant TabSession notifier,
|
||||||
) {
|
) {
|
||||||
return notifier.build(
|
return notifier.build(
|
||||||
tabId,
|
tabId: tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import 'dart:async';
|
|||||||
|
|
||||||
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/features/geckoview/domain/entities/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/database.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/selected_container.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
|
import 'package:lensai/utils/debouncer.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
@@ -57,6 +62,10 @@ class TabRepository extends _$TabRepository {
|
|||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
final eventSerivce = ref.watch(eventServiceProvider);
|
final eventSerivce = ref.watch(eventServiceProvider);
|
||||||
|
final tabContentService = ref.watch(tabContentServiceProvider);
|
||||||
|
|
||||||
|
final tabStateDebouncer = Debouncer(const Duration(seconds: 3));
|
||||||
|
Map<String, TabState>? debounceStartValue;
|
||||||
|
|
||||||
_db = ref.watch(tabDatabaseProvider);
|
_db = ref.watch(tabDatabaseProvider);
|
||||||
|
|
||||||
@@ -67,8 +76,58 @@ class TabRepository extends _$TabRepository {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final tabContentSub =
|
||||||
|
tabContentService.tabContentStream.listen((content) async {
|
||||||
|
await _db.tabDao.updateTab(
|
||||||
|
content.tabId,
|
||||||
|
extractedContentMarkdown: Value(content.extractedContentMarkdown),
|
||||||
|
extractedContentPlain: Value(content.extractedContentPlain),
|
||||||
|
fullContentMarkdown: Value(content.fullContentMarkdown),
|
||||||
|
fullContentPlain: Value(content.fullContentPlain),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
selectedTabProvider,
|
||||||
|
(previous, tabId) async {
|
||||||
|
if (tabId != null) {
|
||||||
|
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
tabListProvider,
|
||||||
|
(previous, next) async {
|
||||||
|
//Only sync tabs if there has been a previous value or is not empty
|
||||||
|
final syncTabs = next.isNotEmpty || (previous?.isNotEmpty ?? false);
|
||||||
|
|
||||||
|
if (syncTabs) {
|
||||||
|
await _db.tabDao.syncTabs(retainTabIds: next);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
tabStatesProvider,
|
||||||
|
(previous, next) async {
|
||||||
|
//Since state changes occure pretty often and our map always contains
|
||||||
|
//the latest state, we cache the value before starting debouncing and
|
||||||
|
//later diff to that, to avoid frequent database writes
|
||||||
|
if (!tabStateDebouncer.isDebouncing) {
|
||||||
|
debounceStartValue = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabStateDebouncer.eventOccured(() async {
|
||||||
|
await _db.tabDao.updateTabs(debounceStartValue, next);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
|
tabStateDebouncer.dispose();
|
||||||
unawaited(tabAddedSub.cancel());
|
unawaited(tabAddedSub.cancel());
|
||||||
|
unawaited(tabContentSub.cancel());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'05040223b83cd5ff80afaf89407043a10e4777cb';
|
String _$tabRepositoryHash() => r'b151be1c18d87eff3a15f4b70f4e82fcc11047ec';
|
||||||
|
|
||||||
/// See also [TabRepository].
|
/// See also [TabRepository].
|
||||||
@ProviderFor(TabRepository)
|
@ProviderFor(TabRepository)
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/data/models/web_page_info.dart';
|
import 'package:lensai/data/models/web_page_info.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||||
@@ -92,7 +92,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
if (formKey.currentState?.validate() ?? false) {
|
||||||
await ref
|
await ref
|
||||||
.read(tabSessionProvider(null).notifier)
|
.read(tabSessionProvider(tabId: null).notifier)
|
||||||
.loadUrl(
|
.loadUrl(
|
||||||
url: uri_parser.tryParseUrl(
|
url: uri_parser.tryParseUrl(
|
||||||
urlTextController.text,
|
urlTextController.text,
|
||||||
|
|||||||
+31
-49
@@ -19,6 +19,7 @@ import 'package:lensai/features/geckoview/features/browser/domain/services/engin
|
|||||||
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
||||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||||
@@ -47,6 +48,9 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
//Init here to avoid dispose errors of menu items when called
|
||||||
|
final addonService = ref.watch(addonServiceProvider);
|
||||||
|
|
||||||
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
||||||
final displayedOverlayDialog = ref.watch(overlayDialogControllerProvider);
|
final displayedOverlayDialog = ref.watch(overlayDialogControllerProvider);
|
||||||
|
|
||||||
@@ -186,14 +190,15 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
color: (activeTool == KagiTool.search)
|
color: (activeTool == KagiTool.search)
|
||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: null,
|
: null,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
ref
|
// ref
|
||||||
.read(createTabStreamProvider.notifier)
|
// .read(createTabStreamProvider.notifier)
|
||||||
.createTab(
|
// .createTab(
|
||||||
CreateTabSheet(
|
// CreateTabSheet(
|
||||||
preferredTool: KagiTool.search,
|
// preferredTool: KagiTool.search,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
|
await context.push(SearchRoute().location);
|
||||||
},
|
},
|
||||||
icon: Icon(KagiTool.search.icon),
|
icon: Icon(KagiTool.search.icon),
|
||||||
),
|
),
|
||||||
@@ -337,30 +342,12 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
(extension) => IconButton(
|
(extension) => IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
//Use parents .ref because after onPressed this consumer gets disposed already
|
//Use parents .ref because after onPressed this consumer gets disposed already
|
||||||
await ref
|
await addonService.invokeAddonAction(
|
||||||
.read(addonServiceProvider)
|
extension.extensionId,
|
||||||
.invokeAddonAction(
|
WebExtensionActionType.page,
|
||||||
extension.extensionId,
|
);
|
||||||
WebExtensionActionType.page,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
icon: Badge(
|
icon: ExtensionBadgeIcon(extension),
|
||||||
isLabelVisible:
|
|
||||||
extension.badgeText?.isNotEmpty ??
|
|
||||||
false,
|
|
||||||
label: (extension.badgeText?.isNotEmpty ==
|
|
||||||
true)
|
|
||||||
? Text(extension.badgeText!)
|
|
||||||
: null,
|
|
||||||
textColor: extension.badgeTextColor,
|
|
||||||
backgroundColor:
|
|
||||||
extension.badgeBackgroundColor,
|
|
||||||
child: RawImage(
|
|
||||||
image: extension.icon?.value,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -383,8 +370,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
child: const Text('Settings'),
|
child: const Text('Settings'),
|
||||||
),
|
),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, childRef, child) {
|
builder: (context, ref, child) {
|
||||||
final browserExtensions = childRef.watch(
|
final browserExtensions = ref.watch(
|
||||||
webExtensionsStateProvider(
|
webExtensionsStateProvider(
|
||||||
WebExtensionActionType.browser,
|
WebExtensionActionType.browser,
|
||||||
).select((value) => value.values.toList()),
|
).select((value) => value.values.toList()),
|
||||||
@@ -396,24 +383,18 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
(extension) => MenuItemButton(
|
(extension) => MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
//Use parents .ref because after onPressed this consumer gets disposed already
|
//Use parents .ref because after onPressed this consumer gets disposed already
|
||||||
await ref
|
await addonService.invokeAddonAction(
|
||||||
.read(addonServiceProvider)
|
extension.extensionId,
|
||||||
.invokeAddonAction(
|
WebExtensionActionType.browser,
|
||||||
extension.extensionId,
|
);
|
||||||
WebExtensionActionType.browser,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
leadingIcon: RawImage(
|
leadingIcon: ExtensionBadgeIcon(extension),
|
||||||
image: extension.icon?.value,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
),
|
|
||||||
child: Text(extension.title ?? ''),
|
child: Text(extension.title ?? ''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await childRef
|
await ref
|
||||||
.read(addonServiceProvider)
|
.read(addonServiceProvider)
|
||||||
.startAddonManagerActivity();
|
.startAddonManagerActivity();
|
||||||
},
|
},
|
||||||
@@ -530,7 +511,8 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
tabSessionProvider(selectedTabId).notifier,
|
tabSessionProvider(tabId: selectedTabId)
|
||||||
|
.notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
await controller.reload();
|
await controller.reload();
|
||||||
@@ -556,7 +538,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
tabSessionProvider(
|
tabSessionProvider(
|
||||||
selectedTabId,
|
tabId: selectedTabId,
|
||||||
).notifier,
|
).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -588,7 +570,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
? () async {
|
? () async {
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
tabSessionProvider(
|
tabSessionProvider(
|
||||||
selectedTabId,
|
tabId: selectedTabId,
|
||||||
).notifier,
|
).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -664,7 +646,7 @@ class BrowserScreen extends HookConsumerWidget {
|
|||||||
lastBackButtonPress.value = null;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
final controller = ref.read(
|
final controller = ref.read(
|
||||||
tabSessionProvider(selectedTabId).notifier,
|
tabSessionProvider(tabId: selectedTabId).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
await controller.goBack();
|
await controller.goBack();
|
||||||
@@ -7,6 +7,7 @@ import 'package:lensai/core/logger.dart';
|
|||||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
|
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
|
||||||
class BrowserView extends StatefulHookConsumerWidget {
|
class BrowserView extends StatefulHookConsumerWidget {
|
||||||
final Duration screenshotPeriod;
|
final Duration screenshotPeriod;
|
||||||
@@ -84,6 +85,8 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
|||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
//Initialize and register dependencies
|
//Initialize and register dependencies
|
||||||
|
ref.listenManual(tabRepositoryProvider, (previous, next) {});
|
||||||
|
|
||||||
ref.listenManual(
|
ref.listenManual(
|
||||||
selectionActionServiceProvider,
|
selectionActionServiceProvider,
|
||||||
(previous, next) {},
|
(previous, next) {},
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/entities/web_extension_state.dart';
|
||||||
|
|
||||||
|
class ExtensionBadgeIcon extends StatelessWidget {
|
||||||
|
final WebExtensionState state;
|
||||||
|
|
||||||
|
const ExtensionBadgeIcon(this.state);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasBadge = state.badgeText?.isNotEmpty ?? false;
|
||||||
|
|
||||||
|
return Badge(
|
||||||
|
isLabelVisible: hasBadge,
|
||||||
|
label: hasBadge ? Text(state.badgeText!) : null,
|
||||||
|
textColor: state.badgeTextColor,
|
||||||
|
backgroundColor: state.badgeBackgroundColor,
|
||||||
|
child: RawImage(
|
||||||
|
image: state.icon?.value,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-1
@@ -67,7 +67,31 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ContainerChips(),
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
|
child: Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final selectedContainer = ref.watch(
|
||||||
|
selectedContainerDataProvider
|
||||||
|
.select((value) => value.valueOrNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ContainerChips(
|
||||||
|
selectedContainer: selectedContainer,
|
||||||
|
onSelected: (container) {
|
||||||
|
ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.setContainerId(container.id);
|
||||||
|
},
|
||||||
|
onDeleted: (container) async {
|
||||||
|
ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.clearContainer();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:lensai/core/logger.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||||
import 'package:lexo_rank/lexo_rank.dart';
|
import 'package:lexo_rank/lexo_rank.dart';
|
||||||
|
|
||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
@@ -10,6 +14,14 @@ part 'tab.g.dart';
|
|||||||
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||||
TabDao(super.db);
|
TabDao(super.db);
|
||||||
|
|
||||||
|
UpdateStatement<Tab, TabData> _updateByIdStatement(String id) =>
|
||||||
|
db.tab.update()..where((t) => t.id.equals(id));
|
||||||
|
|
||||||
|
Selectable<TabData> allTabData() => db.tab.select();
|
||||||
|
|
||||||
|
SingleOrNullSelectable<TabData> tabDataById(String id) =>
|
||||||
|
db.tab.select()..where((t) => t.id.equals(id));
|
||||||
|
|
||||||
Selectable<String> containerTabIds(String? containerId) {
|
Selectable<String> containerTabIds(String? containerId) {
|
||||||
final query = selectOnly(db.tab)
|
final query = selectOnly(db.tab)
|
||||||
..addColumns([db.tab.id])
|
..addColumns([db.tab.id])
|
||||||
@@ -74,7 +86,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
String id, {
|
String id, {
|
||||||
required String? containerId,
|
required String? containerId,
|
||||||
}) {
|
}) {
|
||||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
final statement = _updateByIdStatement(id);
|
||||||
return statement.write(
|
return statement.write(
|
||||||
TabCompanion(containerId: Value(containerId)),
|
TabCompanion(containerId: Value(containerId)),
|
||||||
);
|
);
|
||||||
@@ -84,7 +96,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
String id, {
|
String id, {
|
||||||
required String orderKey,
|
required String orderKey,
|
||||||
}) {
|
}) {
|
||||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
final statement = _updateByIdStatement(id);
|
||||||
return statement.write(
|
return statement.write(
|
||||||
TabCompanion(orderKey: Value(orderKey)),
|
TabCompanion(orderKey: Value(orderKey)),
|
||||||
);
|
);
|
||||||
@@ -94,12 +106,71 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
String id, {
|
String id, {
|
||||||
required DateTime timestamp,
|
required DateTime timestamp,
|
||||||
}) {
|
}) {
|
||||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
final statement = _updateByIdStatement(id);
|
||||||
return statement.write(
|
return statement.write(
|
||||||
TabCompanion(timestamp: Value(timestamp)),
|
TabCompanion(timestamp: Value(timestamp)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateTab(
|
||||||
|
String id, {
|
||||||
|
Value<String?> url = const Value.absent(),
|
||||||
|
Value<String?> title = const Value.absent(),
|
||||||
|
Value<String?> extractedContentMarkdown = const Value.absent(),
|
||||||
|
Value<String?> extractedContentPlain = const Value.absent(),
|
||||||
|
Value<String?> fullContentMarkdown = const Value.absent(),
|
||||||
|
Value<String?> fullContentPlain = const Value.absent(),
|
||||||
|
}) async {
|
||||||
|
final doUpdate = url != const Value.absent() ||
|
||||||
|
title != const Value.absent() ||
|
||||||
|
extractedContentMarkdown != const Value.absent() ||
|
||||||
|
extractedContentPlain != const Value.absent() ||
|
||||||
|
fullContentMarkdown != const Value.absent() ||
|
||||||
|
fullContentPlain != const Value.absent();
|
||||||
|
|
||||||
|
if (doUpdate) {
|
||||||
|
final statement = _updateByIdStatement(id);
|
||||||
|
await statement.write(
|
||||||
|
TabCompanion(
|
||||||
|
url: url,
|
||||||
|
title: title,
|
||||||
|
extractedContentMarkdown: extractedContentMarkdown,
|
||||||
|
extractedContentPlain: extractedContentPlain,
|
||||||
|
fullContentMarkdown: fullContentMarkdown,
|
||||||
|
fullContentPlain: fullContentPlain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateTabs(
|
||||||
|
Map<String, TabState>? previous,
|
||||||
|
Map<String, TabState> next,
|
||||||
|
) async {
|
||||||
|
await batch((batch) async {
|
||||||
|
for (final state in next.values) {
|
||||||
|
final previousState = previous?[state.id];
|
||||||
|
|
||||||
|
if (previousState == null ||
|
||||||
|
previousState.url != state.url ||
|
||||||
|
previousState.title != state.title) {
|
||||||
|
batch.update(
|
||||||
|
db.tab,
|
||||||
|
TabCompanion(
|
||||||
|
url: (previousState?.url != state.url)
|
||||||
|
? Value(state.url.toString())
|
||||||
|
: const Value.absent(),
|
||||||
|
title: (previousState?.title != state.title)
|
||||||
|
? Value(state.title)
|
||||||
|
: const Value.absent(),
|
||||||
|
),
|
||||||
|
where: (t) => t.id.equals(state.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> syncTabs({required List<String> retainTabIds}) async {
|
Future<void> syncTabs({required List<String> retainTabIds}) async {
|
||||||
return db.transaction(() async {
|
return db.transaction(() async {
|
||||||
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
|
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
|
||||||
@@ -125,4 +196,30 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<TabQueryResult> queryTabs({
|
||||||
|
required String matchPrefix,
|
||||||
|
required String matchSuffix,
|
||||||
|
required String ellipsis,
|
||||||
|
required int snippetLength,
|
||||||
|
required String searchString,
|
||||||
|
}) {
|
||||||
|
final ftsQuery = db.buildFtsQuery(searchString);
|
||||||
|
|
||||||
|
if (ftsQuery.isNotEmpty) {
|
||||||
|
return db.queryTabsFullContent(
|
||||||
|
query: db.buildFtsQuery(searchString),
|
||||||
|
snippetLength: snippetLength,
|
||||||
|
beforeMatch: matchPrefix,
|
||||||
|
afterMatch: matchSuffix,
|
||||||
|
ellipsis: ellipsis,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return db.queryTabsBasic(
|
||||||
|
query: db.buildLikeQuery(searchString),
|
||||||
|
beforeMatch: matchPrefix,
|
||||||
|
afterMatch: matchSuffix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import 'package:lensai/data/database/converters/icon_data.dart';
|
|||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
|
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.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/models/tab_query_result.dart';
|
||||||
|
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||||
|
import 'package:lensai/features/search/domain/fts_tokenizer.dart';
|
||||||
|
import 'package:lensai/features/search/domain/unix_tokenizer.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@@ -13,10 +17,40 @@ part 'database.g.dart';
|
|||||||
include: {'database.drift'},
|
include: {'database.drift'},
|
||||||
daos: [ContainerDao, TabDao],
|
daos: [ContainerDao, TabDao],
|
||||||
)
|
)
|
||||||
class TabDatabase extends _$TabDatabase {
|
class TabDatabase extends _$TabDatabase implements IQueryBuilder {
|
||||||
@override
|
@override
|
||||||
final int schemaVersion = 2;
|
final int schemaVersion = 2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int ftsTokenLimit = 10;
|
||||||
|
@override
|
||||||
|
final int ftsMinTokenLength = 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String buildFtsQuery(String input) {
|
||||||
|
final ftsQueryBuilder = TrigramQueryBuilder.tokenize(
|
||||||
|
input: input,
|
||||||
|
minTokenLength: ftsMinTokenLength,
|
||||||
|
tokenLimit: ftsTokenLimit,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ftsQueryBuilder.hasTokens) {
|
||||||
|
return ftsQueryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildLikeQuery(String input) {
|
||||||
|
final likeQueryBuilder = UnixLikeQueryBuilder.tokenize(
|
||||||
|
input: input,
|
||||||
|
minTokenLength: 1,
|
||||||
|
tokenLimit: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
return likeQueryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
onUpgrade: (m, from, to) async {
|
onUpgrade: (m, from, to) async {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:lensai/data/database/converters/color.dart';
|
import 'package:lensai/data/database/converters/color.dart';
|
||||||
import 'package:lensai/data/database/converters/icon_data.dart';
|
import 'package:lensai/data/database/converters/icon_data.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/models/tab_query_result.dart';
|
||||||
|
|
||||||
CREATE TABLE container (
|
CREATE TABLE container (
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
@@ -14,9 +15,45 @@ CREATE TABLE tab (
|
|||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
container_id TEXT REFERENCES container (id) ON DELETE CASCADE,
|
container_id TEXT REFERENCES container (id) ON DELETE CASCADE,
|
||||||
order_key TEXT NOT NULL,
|
order_key TEXT NOT NULL,
|
||||||
|
url TEXT,
|
||||||
|
title TEXT,
|
||||||
|
extracted_content_markdown TEXT,
|
||||||
|
extracted_content_plain TEXT,
|
||||||
|
full_content_markdown TEXT,
|
||||||
|
full_content_plain TEXT,
|
||||||
timestamp DATETIME NOT NULL
|
timestamp DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE VIRTUAL TABLE tab_fts
|
||||||
|
USING fts5(
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
extracted_content_plain,
|
||||||
|
full_content_plain,
|
||||||
|
content=tab,
|
||||||
|
tokenize="trigram"
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Triggers to keep the FTS index up to date.
|
||||||
|
CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN
|
||||||
|
INSERT INTO
|
||||||
|
tab_fts(rowid, title, url, extracted_content_plain, full_content_plain)
|
||||||
|
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN
|
||||||
|
INSERT INTO
|
||||||
|
tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain)
|
||||||
|
VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN
|
||||||
|
INSERT INTO
|
||||||
|
tab_fts(tab_fts, rowid, title, url, extracted_content_plain, full_content_plain)
|
||||||
|
VALUES('delete', old.rowid, old.title, old.url, old.extracted_content_plain, old.full_content_plain);
|
||||||
|
INSERT INTO
|
||||||
|
tab_fts(rowid, title, url, extracted_content_plain, full_content_plain)
|
||||||
|
VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain);
|
||||||
|
END;
|
||||||
|
|
||||||
containersWithCount WITH ContainerDataWithCount:
|
containersWithCount WITH ContainerDataWithCount:
|
||||||
SELECT
|
SELECT
|
||||||
container.*,
|
container.*,
|
||||||
@@ -66,4 +103,53 @@ orderKeyAfterTab(:tab_id AS TEXT, :container_id AS TEXT OR NULL):
|
|||||||
)
|
)
|
||||||
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
||||||
FROM ordered_table
|
FROM ordered_table
|
||||||
WHERE id = :tab_id;
|
WHERE id = :tab_id;
|
||||||
|
|
||||||
|
queryTabsBasic WITH TabQueryResult:
|
||||||
|
WITH weights AS (
|
||||||
|
SELECT
|
||||||
|
-- Customize these weights (higher = more important)
|
||||||
|
10.0 as title_weight, -- Title matches are most important
|
||||||
|
5.0 as url_weight -- URL matches are quite important
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||||
|
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||||
|
bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank
|
||||||
|
FROM tab_fts fts
|
||||||
|
INNER JOIN
|
||||||
|
tab t ON t.rowid = fts.rowid
|
||||||
|
CROSS JOIN weights
|
||||||
|
WHERE
|
||||||
|
fts.title LIKE :query OR
|
||||||
|
fts.url LIKE :query
|
||||||
|
ORDER BY
|
||||||
|
weighted_rank ASC,
|
||||||
|
t.timestamp DESC;
|
||||||
|
|
||||||
|
queryTabsFullContent WITH TabQueryResult:
|
||||||
|
WITH weights AS (
|
||||||
|
SELECT
|
||||||
|
-- Customize these weights (higher = more important)
|
||||||
|
10.0 as title_weight, -- Title matches are most important
|
||||||
|
5.0 as url_weight, -- URL matches are quite important
|
||||||
|
3.0 as extracted_weight, -- Extracted content matches
|
||||||
|
1.0 as full_weight -- Full content matches less important
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||||
|
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||||
|
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
||||||
|
snippet(tab_fts, 3, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS full_content,
|
||||||
|
(
|
||||||
|
bm25(tab_fts, weights.title_weight, weights.url_weight,
|
||||||
|
weights.extracted_weight, weights.full_weight)
|
||||||
|
) AS weighted_rank
|
||||||
|
FROM tab_fts(:query) fts
|
||||||
|
INNER JOIN
|
||||||
|
tab t ON t.rowid = fts.rowid
|
||||||
|
CROSS JOIN weights
|
||||||
|
ORDER BY
|
||||||
|
weighted_rank ASC,
|
||||||
|
t.timestamp DESC;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
class TabQueryResult {
|
||||||
|
final String title;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
final String? extractedContent;
|
||||||
|
final String? fullContent;
|
||||||
|
|
||||||
|
final double weightedRank;
|
||||||
|
|
||||||
|
TabQueryResult({
|
||||||
|
required this.title,
|
||||||
|
required this.url,
|
||||||
|
this.extractedContent,
|
||||||
|
this.fullContent,
|
||||||
|
required this.weightedRank,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:lensai/features/geckoview/features/tabs/data/models/container_da
|
|||||||
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/repositories/container.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart';
|
import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart';
|
||||||
|
import 'package:lensai/features/search/util/tokenized_filter.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -35,6 +36,28 @@ Stream<List<ContainerDataWithCount>> containersWithCount(
|
|||||||
return db.containersWithCount().watch();
|
return db.containersWithCount().watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
|
||||||
|
Ref ref,
|
||||||
|
String? searchText,
|
||||||
|
) {
|
||||||
|
return ref.watch(
|
||||||
|
containersWithCountProvider.select((value) {
|
||||||
|
if (searchText?.isEmpty ?? true) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.whenData(
|
||||||
|
(cb) => TokenizedFilter(
|
||||||
|
items: cb,
|
||||||
|
toString: (item) => item.name,
|
||||||
|
query: searchText!,
|
||||||
|
).filtered,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<String?> tabContainerId(Ref ref, String tabId) {
|
Stream<String?> tabContainerId(Ref ref, String tabId) {
|
||||||
final db = ref.watch(tabDatabaseProvider);
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ final containersWithCountProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef ContainersWithCountRef
|
typedef ContainersWithCountRef
|
||||||
= AutoDisposeStreamProviderRef<List<ContainerDataWithCount>>;
|
= AutoDisposeStreamProviderRef<List<ContainerDataWithCount>>;
|
||||||
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
|
String _$filteredContainersWithCountHash() =>
|
||||||
|
r'3a7284df29182a32f9b7831867a3a0b3e171b382';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -68,6 +69,145 @@ class _SystemHash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
@ProviderFor(filteredContainersWithCount)
|
||||||
|
const filteredContainersWithCountProvider = FilteredContainersWithCountFamily();
|
||||||
|
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
class FilteredContainersWithCountFamily
|
||||||
|
extends Family<AsyncValue<List<ContainerDataWithCount>>> {
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
const FilteredContainersWithCountFamily();
|
||||||
|
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
FilteredContainersWithCountProvider call(
|
||||||
|
String? searchText,
|
||||||
|
) {
|
||||||
|
return FilteredContainersWithCountProvider(
|
||||||
|
searchText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FilteredContainersWithCountProvider getProviderOverride(
|
||||||
|
covariant FilteredContainersWithCountProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.searchText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'filteredContainersWithCountProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
class FilteredContainersWithCountProvider
|
||||||
|
extends AutoDisposeProvider<AsyncValue<List<ContainerDataWithCount>>> {
|
||||||
|
/// See also [filteredContainersWithCount].
|
||||||
|
FilteredContainersWithCountProvider(
|
||||||
|
String? searchText,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => filteredContainersWithCount(
|
||||||
|
ref as FilteredContainersWithCountRef,
|
||||||
|
searchText,
|
||||||
|
),
|
||||||
|
from: filteredContainersWithCountProvider,
|
||||||
|
name: r'filteredContainersWithCountProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$filteredContainersWithCountHash,
|
||||||
|
dependencies: FilteredContainersWithCountFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
FilteredContainersWithCountFamily._allTransitiveDependencies,
|
||||||
|
searchText: searchText,
|
||||||
|
);
|
||||||
|
|
||||||
|
FilteredContainersWithCountProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.searchText,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? searchText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
AsyncValue<List<ContainerDataWithCount>> Function(
|
||||||
|
FilteredContainersWithCountRef provider)
|
||||||
|
create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: FilteredContainersWithCountProvider._internal(
|
||||||
|
(ref) => create(ref as FilteredContainersWithCountRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
searchText: searchText,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
|
||||||
|
createElement() {
|
||||||
|
return _FilteredContainersWithCountProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is FilteredContainersWithCountProvider &&
|
||||||
|
other.searchText == searchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, searchText.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin FilteredContainersWithCountRef
|
||||||
|
on AutoDisposeProviderRef<AsyncValue<List<ContainerDataWithCount>>> {
|
||||||
|
/// The parameter `searchText` of this provider.
|
||||||
|
String? get searchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilteredContainersWithCountProviderElement
|
||||||
|
extends AutoDisposeProviderElement<AsyncValue<List<ContainerDataWithCount>>>
|
||||||
|
with FilteredContainersWithCountRef {
|
||||||
|
_FilteredContainersWithCountProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get searchText =>
|
||||||
|
(origin as FilteredContainersWithCountProvider).searchText;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
|
||||||
|
|
||||||
/// See also [tabContainerId].
|
/// See also [tabContainerId].
|
||||||
@ProviderFor(tabContainerId)
|
@ProviderFor(tabContainerId)
|
||||||
const tabContainerIdProvider = TabContainerIdFamily();
|
const tabContainerIdProvider = TabContainerIdFamily();
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'tab_search.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class TabSearchRepository extends _$TabSearchRepository {
|
||||||
|
late TabDatabase _db;
|
||||||
|
|
||||||
|
Future<void> addQuery(
|
||||||
|
String input, {
|
||||||
|
int snippetLength = 120,
|
||||||
|
String matchPrefix = '***',
|
||||||
|
String matchSuffix = '***',
|
||||||
|
String ellipsis = '…',
|
||||||
|
}) async {
|
||||||
|
if (input.isNotEmpty) {
|
||||||
|
state = await AsyncValue.guard(
|
||||||
|
() => _db.tabDao
|
||||||
|
.queryTabs(
|
||||||
|
matchPrefix: matchPrefix,
|
||||||
|
matchSuffix: matchSuffix,
|
||||||
|
ellipsis: ellipsis,
|
||||||
|
snippetLength: snippetLength,
|
||||||
|
searchString: input,
|
||||||
|
)
|
||||||
|
.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TabQueryResult>> build() {
|
||||||
|
_db = ref.watch(tabDatabaseProvider);
|
||||||
|
|
||||||
|
return Future.value([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-11
@@ -1,27 +1,27 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'autosuggest.dart';
|
part of 'tab_search.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$autosuggestRepositoryHash() =>
|
String _$tabSearchRepositoryHash() =>
|
||||||
r'48d6bf45d648871f8570edfe1e0b394019084695';
|
r'211ca4d7e9a744b5f8a32970ba3a6df721fc05e1';
|
||||||
|
|
||||||
/// See also [AutosuggestRepository].
|
/// See also [TabSearchRepository].
|
||||||
@ProviderFor(AutosuggestRepository)
|
@ProviderFor(TabSearchRepository)
|
||||||
final autosuggestRepositoryProvider =
|
final tabSearchRepositoryProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
NotifierProvider<AutosuggestRepository, Raw<Stream<List<String>>>>.internal(
|
TabSearchRepository, List<TabQueryResult>>.internal(
|
||||||
AutosuggestRepository.new,
|
TabSearchRepository.new,
|
||||||
name: r'autosuggestRepositoryProvider',
|
name: r'tabSearchRepositoryProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
? null
|
? null
|
||||||
: _$autosuggestRepositoryHash,
|
: _$tabSearchRepositoryHash,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$AutosuggestRepository = Notifier<Raw<Stream<List<String>>>>;
|
typedef _$TabSearchRepository = AutoDisposeAsyncNotifier<List<TabQueryResult>>;
|
||||||
// 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
|
||||||
+42
-27
@@ -1,25 +1,44 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
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/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
|
||||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
class ContainerChips extends HookConsumerWidget {
|
class ContainerChips extends HookConsumerWidget {
|
||||||
|
final bool displayMenu;
|
||||||
|
|
||||||
|
final ContainerData? selectedContainer;
|
||||||
|
final void Function(ContainerDataWithCount)? onSelected;
|
||||||
|
final void Function(ContainerDataWithCount)? onDeleted;
|
||||||
|
|
||||||
|
final TextEditingController? searchTextController;
|
||||||
|
|
||||||
|
const ContainerChips({
|
||||||
|
required this.selectedContainer,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.onDeleted,
|
||||||
|
this.searchTextController,
|
||||||
|
this.displayMenu = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final containersAsync = ref.watch(containersWithCountProvider);
|
final searchText = useListenableSelector(
|
||||||
final selectedContainer = ref.watch(
|
searchTextController,
|
||||||
selectedContainerDataProvider.select((value) => value.valueOrNull),
|
() => searchTextController?.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final containersAsync =
|
||||||
|
ref.watch(filteredContainersWithCountProvider(searchText));
|
||||||
|
|
||||||
return containersAsync.when(
|
return containersAsync.when(
|
||||||
data: (availableContainers) => SizedBox(
|
data: (availableContainers) => SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
|
||||||
if (selectedContainer != null || availableContainers.isNotEmpty)
|
if (selectedContainer != null || availableContainers.isNotEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SelectableChips(
|
child: SelectableChips(
|
||||||
@@ -38,34 +57,30 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
itemBadgeCount: (container) => container.tabCount,
|
itemBadgeCount: (container) => container.tabCount,
|
||||||
availableItems: availableContainers,
|
availableItems: availableContainers,
|
||||||
selectedItem: selectedContainer,
|
selectedItem: selectedContainer,
|
||||||
onSelected: (container) {
|
onSelected: onSelected,
|
||||||
ref
|
onDeleted: onDeleted,
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.setContainerId(container.id);
|
|
||||||
},
|
|
||||||
onDeleted: (container) async {
|
|
||||||
ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.clearContainer();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Expanded(
|
Visibility(
|
||||||
child: Text(
|
visible: displayMenu,
|
||||||
"Press '>' to manage Containers.",
|
child: Expanded(
|
||||||
style: TextStyle(
|
child: Text(
|
||||||
color: Theme.of(context).hintColor,
|
"Press '>' to manage Containers.",
|
||||||
fontStyle: FontStyle.italic,
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
if (displayMenu)
|
||||||
onPressed: () async {
|
IconButton(
|
||||||
await context.push(ContainerListRoute().location);
|
onPressed: () async {
|
||||||
},
|
await context.push(ContainerListRoute().location);
|
||||||
icon: const Icon(Icons.chevron_right),
|
},
|
||||||
),
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import 'dart:convert';
|
|||||||
import 'package:exceptions/exceptions.dart';
|
import 'package:exceptions/exceptions.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:lensai/core/http_error_handler.dart';
|
import 'package:lensai/core/http_error_handler.dart';
|
||||||
|
import 'package:lensai/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'autosuggest.g.dart';
|
part 'autosuggest.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class KagiAutosuggestService extends _$KagiAutosuggestService {
|
class KagiAutosuggestService extends _$KagiAutosuggestService
|
||||||
|
implements ISearchSuggestionProvider {
|
||||||
static final _baseUrl = Uri.https('kagi.com', 'api/autosuggest');
|
static final _baseUrl = Uri.https('kagi.com', 'api/autosuggest');
|
||||||
|
|
||||||
late http.Client _client;
|
late http.Client _client;
|
||||||
@@ -18,6 +20,7 @@ class KagiAutosuggestService extends _$KagiAutosuggestService {
|
|||||||
_client = http.Client();
|
_client = http.Client();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<Result<List<String>>> getSuggestions(String query) async {
|
Future<Result<List<String>>> getSuggestions(String query) async {
|
||||||
return Result.fromAsync(
|
return Result.fromAsync(
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'autosuggest.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$kagiAutosuggestServiceHash() =>
|
String _$kagiAutosuggestServiceHash() =>
|
||||||
r'843b0d84b1dde2d903412a123c8c10d26e8036fd';
|
r'e1cb4ef730bcf5c4bf4c77617bb12a22228af576';
|
||||||
|
|
||||||
/// See also [KagiAutosuggestService].
|
/// See also [KagiAutosuggestService].
|
||||||
@ProviderFor(KagiAutosuggestService)
|
@ProviderFor(KagiAutosuggestService)
|
||||||
|
|||||||
+8
-7
@@ -1,26 +1,28 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:lensai/features/kagi/data/services/autosuggest.dart';
|
import 'package:lensai/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
|
||||||
import 'package:lensai/utils/lru_cache.dart';
|
import 'package:lensai/utils/lru_cache.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
part 'autosuggest.g.dart';
|
part 'search_suggestions.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class AutosuggestRepository extends _$AutosuggestRepository {
|
class SearchSuggestionsRepository extends _$SearchSuggestionsRepository {
|
||||||
final LRUCache<String, List<String>> _cache;
|
final LRUCache<String, List<String>> _cache;
|
||||||
|
|
||||||
late StreamController<String> _queryStreamController;
|
late StreamController<String> _queryStreamController;
|
||||||
|
|
||||||
AutosuggestRepository() : _cache = LRUCache(100);
|
SearchSuggestionsRepository() : _cache = LRUCache(100);
|
||||||
|
|
||||||
void addQuery(String query) {
|
void addQuery(String query) {
|
||||||
_queryStreamController.add(query);
|
_queryStreamController.add(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Raw<Stream<List<String>>> build() {
|
Raw<Stream<List<String>>> build(
|
||||||
|
ISearchSuggestionProvider suggestionsProvider,
|
||||||
|
) {
|
||||||
_queryStreamController = StreamController();
|
_queryStreamController = StreamController();
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
await _queryStreamController.close();
|
await _queryStreamController.close();
|
||||||
@@ -39,8 +41,7 @@ class AutosuggestRepository extends _$AutosuggestRepository {
|
|||||||
return Stream.value(cached);
|
return Stream.value(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref
|
return suggestionsProvider
|
||||||
.read(kagiAutosuggestServiceProvider.notifier)
|
|
||||||
// ignore: discarded_futures
|
// ignore: discarded_futures
|
||||||
.getSuggestions(query)
|
.getSuggestions(query)
|
||||||
// ignore: discarded_futures
|
// ignore: discarded_futures
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'search_suggestions.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$searchSuggestionsRepositoryHash() =>
|
||||||
|
r'bc6dc862ae7d887b404972abbbee3c1b27a64d2e';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$SearchSuggestionsRepository
|
||||||
|
extends BuildlessNotifier<Raw<Stream<List<String>>>> {
|
||||||
|
late final ISearchSuggestionProvider suggestionsProvider;
|
||||||
|
|
||||||
|
Raw<Stream<List<String>>> build(
|
||||||
|
ISearchSuggestionProvider suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
@ProviderFor(SearchSuggestionsRepository)
|
||||||
|
const searchSuggestionsRepositoryProvider = SearchSuggestionsRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
class SearchSuggestionsRepositoryFamily
|
||||||
|
extends Family<Raw<Stream<List<String>>>> {
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
const SearchSuggestionsRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
SearchSuggestionsRepositoryProvider call(
|
||||||
|
ISearchSuggestionProvider suggestionsProvider,
|
||||||
|
) {
|
||||||
|
return SearchSuggestionsRepositoryProvider(
|
||||||
|
suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SearchSuggestionsRepositoryProvider getProviderOverride(
|
||||||
|
covariant SearchSuggestionsRepositoryProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'searchSuggestionsRepositoryProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
class SearchSuggestionsRepositoryProvider extends NotifierProviderImpl<
|
||||||
|
SearchSuggestionsRepository, Raw<Stream<List<String>>>> {
|
||||||
|
/// See also [SearchSuggestionsRepository].
|
||||||
|
SearchSuggestionsRepositoryProvider(
|
||||||
|
ISearchSuggestionProvider suggestionsProvider,
|
||||||
|
) : this._internal(
|
||||||
|
() => SearchSuggestionsRepository()
|
||||||
|
..suggestionsProvider = suggestionsProvider,
|
||||||
|
from: searchSuggestionsRepositoryProvider,
|
||||||
|
name: r'searchSuggestionsRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$searchSuggestionsRepositoryHash,
|
||||||
|
dependencies: SearchSuggestionsRepositoryFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SearchSuggestionsRepositoryFamily._allTransitiveDependencies,
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
SearchSuggestionsRepositoryProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.suggestionsProvider,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final ISearchSuggestionProvider suggestionsProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Raw<Stream<List<String>>> runNotifierBuild(
|
||||||
|
covariant SearchSuggestionsRepository notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(SearchSuggestionsRepository Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SearchSuggestionsRepositoryProvider._internal(
|
||||||
|
() => create()..suggestionsProvider = suggestionsProvider,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NotifierProviderElement<SearchSuggestionsRepository,
|
||||||
|
Raw<Stream<List<String>>>> createElement() {
|
||||||
|
return _SearchSuggestionsRepositoryProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SearchSuggestionsRepositoryProvider &&
|
||||||
|
other.suggestionsProvider == suggestionsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, suggestionsProvider.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SearchSuggestionsRepositoryRef
|
||||||
|
on NotifierProviderRef<Raw<Stream<List<String>>>> {
|
||||||
|
/// The parameter `suggestionsProvider` of this provider.
|
||||||
|
ISearchSuggestionProvider get suggestionsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchSuggestionsRepositoryProviderElement
|
||||||
|
extends NotifierProviderElement<SearchSuggestionsRepository,
|
||||||
|
Raw<Stream<List<String>>>> with SearchSuggestionsRepositoryRef {
|
||||||
|
_SearchSuggestionsRepositoryProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ISearchSuggestionProvider get suggestionsProvider =>
|
||||||
|
(origin as SearchSuggestionsRepositoryProvider).suggestionsProvider;
|
||||||
|
}
|
||||||
|
// 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
|
||||||
@@ -4,7 +4,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.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/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
|
import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
|
||||||
@@ -32,8 +32,8 @@ class SearchTab extends HookConsumerWidget {
|
|||||||
|
|
||||||
final selectedBang = ref
|
final selectedBang = ref
|
||||||
.watch(selectedBangDataProvider().select((value) => value.valueOrNull));
|
.watch(selectedBangDataProvider().select((value) => value.valueOrNull));
|
||||||
final defaultSearchBang = ref
|
final defaultSearchBang = ref.watch(
|
||||||
.watch(kagiSearchBangDataProvider.select((value) => value.valueOrNull));
|
defaultSearchBangDataProvider.select((value) => value.valueOrNull));
|
||||||
|
|
||||||
final activeBang = selectedBang ?? defaultSearchBang;
|
final activeBang = selectedBang ?? defaultSearchBang;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,5 +2,5 @@ abstract interface class IQueryBuilder {
|
|||||||
int get ftsTokenLimit;
|
int get ftsTokenLimit;
|
||||||
int get ftsMinTokenLength;
|
int get ftsMinTokenLength;
|
||||||
|
|
||||||
String buildQuery(String input);
|
String buildFtsQuery(String input);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import 'package:exceptions/exceptions.dart';
|
||||||
|
|
||||||
|
abstract interface class ISearchSuggestionProvider {
|
||||||
|
Future<Result<List<String>>> getSuggestions(String query);
|
||||||
|
}
|
||||||
+10
-8
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:lensai/features/query/domain/entities/abstract/i_query_builder.dart';
|
import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart';
|
||||||
import 'package:lensai/features/query/domain/entities/bareword.dart';
|
import 'package:lensai/features/search/domain/entities/bareword.dart';
|
||||||
|
|
||||||
typedef _Phrase = List<Bareword>;
|
typedef _Phrase = List<Bareword>;
|
||||||
|
|
||||||
sealed class QueryBuilder {
|
sealed class FtsQueryBuilder {
|
||||||
///Matching quoted strings like "xda bda" (group 1), as well as strings delimetered by
|
///Matching quoted strings like "xda bda" (group 1), as well as strings delimetered by
|
||||||
///a whitespace (group 2)
|
///a whitespace (group 2)
|
||||||
static final _tokenizePattern = RegExp('"([^"]+)"|([^ ]+)');
|
static final _tokenizePattern = RegExp('"([^"]+)"|([^ ]+)');
|
||||||
@@ -26,7 +26,9 @@ sealed class QueryBuilder {
|
|||||||
|
|
||||||
late final _Phrase _tokens;
|
late final _Phrase _tokens;
|
||||||
|
|
||||||
QueryBuilder.tokenize({
|
bool get hasTokens => _tokens.isNotEmpty;
|
||||||
|
|
||||||
|
FtsQueryBuilder.tokenize({
|
||||||
required String input,
|
required String input,
|
||||||
required int minTokenLength,
|
required int minTokenLength,
|
||||||
required int tokenLimit,
|
required int tokenLimit,
|
||||||
@@ -174,7 +176,7 @@ sealed class QueryBuilder {
|
|||||||
String build();
|
String build();
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PrefixQueryBuilder extends QueryBuilder {
|
final class PrefixQueryBuilder extends FtsQueryBuilder {
|
||||||
PrefixQueryBuilder.tokenize({
|
PrefixQueryBuilder.tokenize({
|
||||||
required super.input,
|
required super.input,
|
||||||
required super.minTokenLength,
|
required super.minTokenLength,
|
||||||
@@ -197,7 +199,7 @@ final class PrefixQueryBuilder extends QueryBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class TrigramQueryBuilder extends QueryBuilder {
|
final class TrigramQueryBuilder extends FtsQueryBuilder {
|
||||||
TrigramQueryBuilder.tokenize({
|
TrigramQueryBuilder.tokenize({
|
||||||
required super.input,
|
required super.input,
|
||||||
required super.minTokenLength,
|
required super.minTokenLength,
|
||||||
@@ -216,7 +218,7 @@ final class TrigramQueryBuilder extends QueryBuilder {
|
|||||||
|
|
||||||
mixin PrefixQueryBuilderMixin implements IQueryBuilder {
|
mixin PrefixQueryBuilderMixin implements IQueryBuilder {
|
||||||
@override
|
@override
|
||||||
String buildQuery(String input) {
|
String buildFtsQuery(String input) {
|
||||||
final queryBuilder = PrefixQueryBuilder.tokenize(
|
final queryBuilder = PrefixQueryBuilder.tokenize(
|
||||||
input: input,
|
input: input,
|
||||||
minTokenLength: ftsMinTokenLength,
|
minTokenLength: ftsMinTokenLength,
|
||||||
@@ -229,7 +231,7 @@ mixin PrefixQueryBuilderMixin implements IQueryBuilder {
|
|||||||
|
|
||||||
mixin TrigramQueryBuilderMixin implements IQueryBuilder {
|
mixin TrigramQueryBuilderMixin implements IQueryBuilder {
|
||||||
@override
|
@override
|
||||||
String buildQuery(String input) {
|
String buildFtsQuery(String input) {
|
||||||
final queryBuilder = TrigramQueryBuilder.tokenize(
|
final queryBuilder = TrigramQueryBuilder.tokenize(
|
||||||
input: input,
|
input: input,
|
||||||
minTokenLength: ftsMinTokenLength,
|
minTokenLength: ftsMinTokenLength,
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:lensai/features/kagi/data/services/autosuggest.dart';
|
||||||
|
import 'package:lensai/features/kagi/domain/repositories/search_suggestions.dart';
|
||||||
|
import 'package:lensai/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
|
||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'search_suggestions.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
ISearchSuggestionProvider defaultSearchSuggestions(Ref ref) {
|
||||||
|
return ref.watch(kagiAutosuggestServiceProvider.notifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class SearchSuggestions extends _$SearchSuggestions {
|
||||||
|
late void Function(String query) _addQueryBinding;
|
||||||
|
|
||||||
|
void addQuery(String query) => _addQueryBinding(query);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Raw<Stream<List<String>>> build({
|
||||||
|
ISearchSuggestionProvider? suggestionsProvider,
|
||||||
|
}) {
|
||||||
|
final defaultProvider = ref.watch(defaultSearchSuggestionsProvider);
|
||||||
|
final resolvedProvider = suggestionsProvider ?? defaultProvider;
|
||||||
|
|
||||||
|
_addQueryBinding = ref
|
||||||
|
.watch(searchSuggestionsRepositoryProvider(resolvedProvider).notifier)
|
||||||
|
.addQuery;
|
||||||
|
|
||||||
|
return ref.watch(
|
||||||
|
searchSuggestionsRepositoryProvider(resolvedProvider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'search_suggestions.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$defaultSearchSuggestionsHash() =>
|
||||||
|
r'56514064baa64acf0a28ec34cfad957dd863538b';
|
||||||
|
|
||||||
|
/// See also [defaultSearchSuggestions].
|
||||||
|
@ProviderFor(defaultSearchSuggestions)
|
||||||
|
final defaultSearchSuggestionsProvider =
|
||||||
|
Provider<ISearchSuggestionProvider>.internal(
|
||||||
|
defaultSearchSuggestions,
|
||||||
|
name: r'defaultSearchSuggestionsProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$defaultSearchSuggestionsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef DefaultSearchSuggestionsRef = ProviderRef<ISearchSuggestionProvider>;
|
||||||
|
String _$searchSuggestionsHash() => r'ecc378aca325cc6759292a06d8d4d5ace910eb7f';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$SearchSuggestions
|
||||||
|
extends BuildlessNotifier<Raw<Stream<List<String>>>> {
|
||||||
|
late final ISearchSuggestionProvider? suggestionsProvider;
|
||||||
|
|
||||||
|
Raw<Stream<List<String>>> build({
|
||||||
|
ISearchSuggestionProvider? suggestionsProvider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
@ProviderFor(SearchSuggestions)
|
||||||
|
const searchSuggestionsProvider = SearchSuggestionsFamily();
|
||||||
|
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
class SearchSuggestionsFamily extends Family<Raw<Stream<List<String>>>> {
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
const SearchSuggestionsFamily();
|
||||||
|
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
SearchSuggestionsProvider call({
|
||||||
|
ISearchSuggestionProvider? suggestionsProvider,
|
||||||
|
}) {
|
||||||
|
return SearchSuggestionsProvider(
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SearchSuggestionsProvider getProviderOverride(
|
||||||
|
covariant SearchSuggestionsProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
suggestionsProvider: provider.suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||||
|
|
||||||
|
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||||
|
_allTransitiveDependencies;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get name => r'searchSuggestionsProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
class SearchSuggestionsProvider
|
||||||
|
extends NotifierProviderImpl<SearchSuggestions, Raw<Stream<List<String>>>> {
|
||||||
|
/// See also [SearchSuggestions].
|
||||||
|
SearchSuggestionsProvider({
|
||||||
|
ISearchSuggestionProvider? suggestionsProvider,
|
||||||
|
}) : this._internal(
|
||||||
|
() => SearchSuggestions()..suggestionsProvider = suggestionsProvider,
|
||||||
|
from: searchSuggestionsProvider,
|
||||||
|
name: r'searchSuggestionsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$searchSuggestionsHash,
|
||||||
|
dependencies: SearchSuggestionsFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
SearchSuggestionsFamily._allTransitiveDependencies,
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
SearchSuggestionsProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.suggestionsProvider,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final ISearchSuggestionProvider? suggestionsProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Raw<Stream<List<String>>> runNotifierBuild(
|
||||||
|
covariant SearchSuggestions notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(SearchSuggestions Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: SearchSuggestionsProvider._internal(
|
||||||
|
() => create()..suggestionsProvider = suggestionsProvider,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
suggestionsProvider: suggestionsProvider,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
NotifierProviderElement<SearchSuggestions, Raw<Stream<List<String>>>>
|
||||||
|
createElement() {
|
||||||
|
return _SearchSuggestionsProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SearchSuggestionsProvider &&
|
||||||
|
other.suggestionsProvider == suggestionsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, suggestionsProvider.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin SearchSuggestionsRef on NotifierProviderRef<Raw<Stream<List<String>>>> {
|
||||||
|
/// The parameter `suggestionsProvider` of this provider.
|
||||||
|
ISearchSuggestionProvider? get suggestionsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchSuggestionsProviderElement extends NotifierProviderElement<
|
||||||
|
SearchSuggestions, Raw<Stream<List<String>>>> with SearchSuggestionsRef {
|
||||||
|
_SearchSuggestionsProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ISearchSuggestionProvider? get suggestionsProvider =>
|
||||||
|
(origin as SearchSuggestionsProvider).suggestionsProvider;
|
||||||
|
}
|
||||||
|
// 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
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:lensai/features/search/domain/entities/bareword.dart';
|
||||||
|
|
||||||
|
typedef _Phrase = List<Bareword>;
|
||||||
|
|
||||||
|
sealed class UnixTokenizer {
|
||||||
|
///Matching quoted strings like "xda bda" (group 1), as well as strings delimetered by
|
||||||
|
///a whitespace (group 2)
|
||||||
|
static final _tokenizePattern = RegExp('"([^"]+)"|([^ ]+)');
|
||||||
|
|
||||||
|
late final _Phrase _tokens;
|
||||||
|
|
||||||
|
bool get hasTokens => _tokens.isNotEmpty;
|
||||||
|
|
||||||
|
static void _mergeShortBarewords(
|
||||||
|
List<Bareword> barewords,
|
||||||
|
int minTokenLength,
|
||||||
|
) {
|
||||||
|
for (var i = 0; i < barewords.length; i++) {
|
||||||
|
final bareword = barewords[i];
|
||||||
|
|
||||||
|
if (i != 0) {
|
||||||
|
if (bareword.word.length < minTokenLength) {
|
||||||
|
barewords[i] = barewords[i - 1].join(bareword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnixTokenizer.tokenize({
|
||||||
|
required String input,
|
||||||
|
required int minTokenLength,
|
||||||
|
required int tokenLimit,
|
||||||
|
}) {
|
||||||
|
final matches = _tokenizePattern.allMatches(input);
|
||||||
|
|
||||||
|
final barewords = matches
|
||||||
|
.map((match) {
|
||||||
|
if (match.group(1) != null) {
|
||||||
|
return EnclosedBareword(match.group(1)!);
|
||||||
|
} else {
|
||||||
|
return SimpleBareword(match.group(2)!);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((token) => token.word.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
//Merge short tokens
|
||||||
|
_mergeShortBarewords(barewords, minTokenLength);
|
||||||
|
|
||||||
|
_tokens = barewords.take(tokenLimit).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String build({bool wildcard});
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UnixLikeQueryBuilder extends UnixTokenizer {
|
||||||
|
UnixLikeQueryBuilder.tokenize({
|
||||||
|
required super.input,
|
||||||
|
required super.minTokenLength,
|
||||||
|
required super.tokenLimit,
|
||||||
|
}) : super.tokenize();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String build({bool wildcard = true}) {
|
||||||
|
final joined = _tokens.join('%');
|
||||||
|
return wildcard ? '%$joined%' : joined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/data/models/equatable_iterable.dart';
|
||||||
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
|
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
|
import 'package:lensai/features/search/domain/providers/search_suggestions.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/widgets/bang_chips.dart';
|
||||||
|
import 'package:lensai/features/search/presentation/widgets/search_field.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
|
class SearchScreen extends HookConsumerWidget {
|
||||||
|
final String? initialSearchText;
|
||||||
|
|
||||||
|
const SearchScreen({required this.initialSearchText});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
|
final searchTextController =
|
||||||
|
useTextEditingController(text: initialSearchText);
|
||||||
|
|
||||||
|
useListenableCallback(
|
||||||
|
searchTextController,
|
||||||
|
() async {
|
||||||
|
ref
|
||||||
|
.read(searchSuggestionsProvider().notifier)
|
||||||
|
.addQuery(searchTextController.text);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabSearchRepositoryProvider.notifier)
|
||||||
|
.addQuery(searchTextController.text);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final globalSelectedContainer = ref.watch(
|
||||||
|
selectedContainerDataProvider.select((value) => value.valueOrNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
final defaultSearchBang = ref.watch(
|
||||||
|
defaultSearchBangDataProvider.select((value) => value.valueOrNull),
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedBang = useState<BangData?>(null);
|
||||||
|
final activeBang = selectedBang.value ?? defaultSearchBang;
|
||||||
|
final showBangIcon = useState(false);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
selectedBangDataProvider(),
|
||||||
|
(previous, next) {
|
||||||
|
if (next.hasValue) {
|
||||||
|
if ((previous?.hasValue ?? false) && previous!.value != next.value) {
|
||||||
|
showBangIcon.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedBang.value = next.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedContainer = useState<ContainerData?>(globalSelectedContainer);
|
||||||
|
|
||||||
|
final availableTabs = ref
|
||||||
|
.watch(
|
||||||
|
availableTabIdsProvider(selectedContainer.value?.id).select(
|
||||||
|
(value) => EquatableCollection(value, immutable: true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.collection;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
pinned: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: SearchField(
|
||||||
|
showBangIcon: showBangIcon.value,
|
||||||
|
textEditingController: searchTextController,
|
||||||
|
onFieldSubmitted: (text) {},
|
||||||
|
activeBang: activeBang,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Site Search',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
BangChips(
|
||||||
|
activeBang: activeBang,
|
||||||
|
onSelected: (bang) {
|
||||||
|
ref
|
||||||
|
.read(selectedBangTriggerProvider().notifier)
|
||||||
|
.setTrigger(bang.trigger);
|
||||||
|
},
|
||||||
|
onDeleted: (bang) async {
|
||||||
|
if (ref.read(selectedBangTriggerProvider()) ==
|
||||||
|
bang.trigger) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
selectedBangTriggerProvider().notifier,
|
||||||
|
)
|
||||||
|
.clearTrigger();
|
||||||
|
} else {
|
||||||
|
final dialogResult = await BangChips.resetBangDialog(
|
||||||
|
context,
|
||||||
|
bang.trigger,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dialogResult == true) {
|
||||||
|
await ref
|
||||||
|
.read(bangDataRepositoryProvider.notifier)
|
||||||
|
.resetFrequency(bang.trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchTextController: searchTextController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
HookConsumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final hasSearchText = useListenableSelector(
|
||||||
|
searchTextController,
|
||||||
|
() => searchTextController.text.isNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
final suggestions =
|
||||||
|
useStream(ref.watch(searchSuggestionsProvider()));
|
||||||
|
|
||||||
|
return (!hasSearchText || !suggestions.hasData)
|
||||||
|
? SliverList.builder(
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.history),
|
||||||
|
title: Text('Search History $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SliverList.builder(
|
||||||
|
itemCount: suggestions.data!.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final suggestion = suggestions.data![index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
title: Text(suggestion),
|
||||||
|
onLongPress: () {
|
||||||
|
searchTextController.text = suggestion;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tabs',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
ContainerChips(
|
||||||
|
displayMenu: false,
|
||||||
|
selectedContainer: selectedContainer.value,
|
||||||
|
onSelected: (container) {
|
||||||
|
selectedContainer.value = container;
|
||||||
|
},
|
||||||
|
onDeleted: (container) {
|
||||||
|
selectedContainer.value = null;
|
||||||
|
},
|
||||||
|
searchTextController: searchTextController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tabs = ref.watch(tabSearchRepositoryProvider);
|
||||||
|
|
||||||
|
return SliverSkeletonizer(
|
||||||
|
enabled: tabs.isLoading,
|
||||||
|
child: tabs.when(
|
||||||
|
data: (data) {
|
||||||
|
return SliverList.builder(itemBuilder: itemBuilder);
|
||||||
|
},
|
||||||
|
error: error,
|
||||||
|
loading: () => SliverList.builder(
|
||||||
|
itemCount: tabs.valueOrNull?.length ?? 3,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTile()
|
||||||
|
},),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: availableTabs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return Consumer(
|
||||||
|
key: ValueKey(availableTabs[index]),
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tab =
|
||||||
|
ref.watch(tabStateProvider(availableTabs[index]));
|
||||||
|
|
||||||
|
if (tab == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: RawImage(
|
||||||
|
image: tab.icon?.value,
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
title: Text(tab.title),
|
||||||
|
subtitle: Text(
|
||||||
|
tab.url.toString(),
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/core/routing/routes.dart';
|
||||||
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
|
import 'package:lensai/features/bangs/domain/repositories/search.dart';
|
||||||
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
|
import 'package:lensai/presentation/hooks/listenable_callback.dart';
|
||||||
|
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
|
class BangChips extends HookConsumerWidget {
|
||||||
|
final bool displayMenu;
|
||||||
|
|
||||||
|
final BangData? activeBang;
|
||||||
|
final void Function(BangData)? onSelected;
|
||||||
|
final void Function(BangData)? onDeleted;
|
||||||
|
|
||||||
|
final TextEditingController? searchTextController;
|
||||||
|
|
||||||
|
const BangChips({
|
||||||
|
required this.activeBang,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.onDeleted,
|
||||||
|
this.searchTextController,
|
||||||
|
this.displayMenu = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<bool?> resetBangDialog(
|
||||||
|
BuildContext context,
|
||||||
|
String triggerName,
|
||||||
|
) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
'Reset usage frequency of $triggerName?',
|
||||||
|
),
|
||||||
|
content: const Text(
|
||||||
|
'This will remove the Bang from quick select.',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Reset'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final availableBangs = ref.watch(seamlessBangProviderProvider);
|
||||||
|
|
||||||
|
useListenableCallback(
|
||||||
|
searchTextController,
|
||||||
|
() {
|
||||||
|
ref
|
||||||
|
.read(seamlessBangProviderProvider.notifier)
|
||||||
|
.search(searchTextController!.text);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return availableBangs.when(
|
||||||
|
data: (availableBangs) => SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (activeBang != null || availableBangs.isNotEmpty)
|
||||||
|
Expanded(
|
||||||
|
child: SelectableChips(
|
||||||
|
itemId: (bang) => bang.trigger,
|
||||||
|
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
|
||||||
|
itemLabel: (bang) => Text(bang.websiteName),
|
||||||
|
availableItems: availableBangs,
|
||||||
|
selectedItem: activeBang,
|
||||||
|
onSelected: onSelected,
|
||||||
|
onDeleted: onDeleted,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Visibility(
|
||||||
|
visible: displayMenu,
|
||||||
|
child: Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Press '>' to search Bangs.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (displayMenu)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final searchText = searchTextController?.text.trim();
|
||||||
|
|
||||||
|
await context.push(
|
||||||
|
BangSearchRoute(
|
||||||
|
searchText: (searchText?.isEmpty ?? true)
|
||||||
|
? BangSearchRoute.emptySearchText
|
||||||
|
: searchText!,
|
||||||
|
).location,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox(
|
||||||
|
height: 48,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
|
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
|
||||||
|
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
|
||||||
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
|
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||||
|
|
||||||
|
class SearchField extends HookConsumerWidget {
|
||||||
|
final TextEditingController textEditingController;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final void Function(String)? onFieldSubmitted;
|
||||||
|
|
||||||
|
final BangData? activeBang;
|
||||||
|
final bool showBangIcon;
|
||||||
|
|
||||||
|
const SearchField({
|
||||||
|
super.key,
|
||||||
|
required this.textEditingController,
|
||||||
|
required this.onFieldSubmitted,
|
||||||
|
required this.activeBang,
|
||||||
|
this.focusNode,
|
||||||
|
this.showBangIcon = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final incognitoEnabled = ref.watch(
|
||||||
|
settingsRepositoryProvider.select(
|
||||||
|
(value) => (value.valueOrNull ?? Settings.withDefaults()).incognitoMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final hasText = useListenableSelector(
|
||||||
|
textEditingController,
|
||||||
|
() => textEditingController.text.isNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextFormField(
|
||||||
|
controller: textEditingController,
|
||||||
|
enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
prefixIcon: (showBangIcon && activeBang != null)
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.all(12.0),
|
||||||
|
child: BangIcon(activeBang!, iconSize: 24.0),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
label: const Text('Search'),
|
||||||
|
hintText: 'Ask anything...',
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
suffixIcon: hasText
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
textEditingController.clear();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
)
|
||||||
|
: SpeechToTextButton(
|
||||||
|
onTextReceived: (data) {
|
||||||
|
textEditingController.text = data.toString();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
// validator: (value) {
|
||||||
|
// if (value?.isEmpty ?? true) {
|
||||||
|
// return '';
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
onTapOutside: (focusNode != null)
|
||||||
|
? (event) {
|
||||||
|
focusNode!.unfocus();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
onFieldSubmitted: onFieldSubmitted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
typedef ToString<T> = String? Function(T);
|
||||||
|
|
||||||
|
class TokenizedFilter<T> {
|
||||||
|
final Iterable<T> original;
|
||||||
|
final List<T> filtered;
|
||||||
|
|
||||||
|
static List<String> _tokenize(String input) {
|
||||||
|
return input
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'[^\w\s]'), '') // Remove punctuation
|
||||||
|
.split(RegExp(r'\s+')) // Split on whitespace
|
||||||
|
.where((word) => word.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _matchTokens(
|
||||||
|
List<String> queryTokens,
|
||||||
|
List<String> targetTokens,
|
||||||
|
) {
|
||||||
|
return queryTokens.every(
|
||||||
|
(queryToken) =>
|
||||||
|
targetTokens.any((targetToken) => targetToken.contains(queryToken)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory TokenizedFilter({
|
||||||
|
required Iterable<T> items,
|
||||||
|
required ToString<T> toString,
|
||||||
|
required String query,
|
||||||
|
}) {
|
||||||
|
final queryTokens = _tokenize(query);
|
||||||
|
|
||||||
|
final filtered = items.where((item) {
|
||||||
|
final itemString = toString(item);
|
||||||
|
if (itemString == null || itemString.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final itemTokens = _tokenize(itemString);
|
||||||
|
return _matchTokens(queryTokens, itemTokens);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return TokenizedFilter._(items, filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TokenizedFilter._(this.original, this.filtered);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang.dart';
|
import 'package:lensai/features/bangs/data/models/bang.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
import 'package:lensai/features/kagi/data/entities/modes.dart';
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang.dart';
|
import 'package:lensai/features/bangs/data/models/bang.dart';
|
||||||
import 'package:lensai/features/bangs/domain/providers.dart';
|
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
|
||||||
import 'package:lensai/features/bangs/domain/repositories/sync.dart';
|
import 'package:lensai/features/bangs/domain/repositories/sync.dart';
|
||||||
import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart';
|
import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart';
|
||||||
import 'package:lensai/features/settings/presentation/widgets/sync_details_table.dart';
|
import 'package:lensai/features/settings/presentation/widgets/sync_details_table.dart';
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
void useListenableCallback(Listenable? listenable, void Function() callback) {
|
void useListenableCallback(Listenable? listenable, void Function() callback,
|
||||||
|
[List<Object?>? keys]) {
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
listenable?.addListener(callback);
|
listenable?.addListener(callback);
|
||||||
return () => listenable?.removeListener(callback);
|
return () => listenable?.removeListener(callback);
|
||||||
},
|
},
|
||||||
[listenable],
|
keys ?? [listenable],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
class Debouncer {
|
||||||
|
final Duration debounce;
|
||||||
|
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
|
Debouncer(this.debounce);
|
||||||
|
|
||||||
|
bool get isDebouncing => _timer?.isActive ?? false;
|
||||||
|
|
||||||
|
void eventOccured(void Function() callback) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(debounce, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class LRUCache<K, V> {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(K key, V value) {
|
V set(K key, V value) {
|
||||||
if (_cache.containsKey(key)) {
|
if (_cache.containsKey(key)) {
|
||||||
_cache.remove(key); // Remove the existing item before updating.
|
_cache.remove(key); // Remove the existing item before updating.
|
||||||
} else if (_cache.length == _capacity) {
|
} else if (_cache.length == _capacity) {
|
||||||
@@ -43,6 +43,6 @@ class LRUCache<K, V> {
|
|||||||
); // Explicitly remove the least recently used item if at capacity.
|
); // Explicitly remove the least recently used item if at capacity.
|
||||||
}
|
}
|
||||||
|
|
||||||
_cache[key] = value; // Inserting or updating the item.
|
return _cache[key] = value; // Inserting or updating the item.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -162,10 +162,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cli_util
|
name: cli_util
|
||||||
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -178,10 +178,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_builder
|
name: code_builder
|
||||||
sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
|
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.10.0"
|
version: "4.10.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -234,10 +234,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: csslib
|
name: csslib
|
||||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.2"
|
||||||
custom_lint:
|
custom_lint:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -290,10 +290,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: "4592cea370f1dcd8afc67987c28402797d9ddb4aa3ae372cb675497c0741816c"
|
sha256: "623649abe932fc17bd32e578e7e05f7ac5e7dd0b33e6c8669a0634105d1389bf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.21.1"
|
version: "2.21.2"
|
||||||
dynamic_color:
|
dynamic_color:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -544,10 +544,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
|
sha256: ce89c5a993ca5eea74535f798478502c30a625ecb10a1de4d7fef5cd1bcac2a4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.0"
|
version: "14.4.1"
|
||||||
go_router_builder:
|
go_router_builder:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -600,10 +600,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: html
|
name: html
|
||||||
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.4"
|
version: "0.15.5"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -817,10 +817,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1260,10 +1260,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "0dea215895a4d254401730ca0ba8204b29109a34a99fb06ae559a2b60988d2de"
|
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.13"
|
version: "6.3.14"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1372,10 +1372,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: e1d0cc62e65dc2561f5071fcbccecf58ff20c344f8f3dc7d4922df372a11df1f
|
sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.7.1"
|
version: "5.8.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+4
-4
@@ -26,11 +26,11 @@ dependencies:
|
|||||||
flutter_pdf_text: ^0.7.0
|
flutter_pdf_text: ^0.7.0
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
flutter_sharing_intent: ^1.1.1
|
flutter_sharing_intent: ^1.1.1
|
||||||
go_router: ^14.3.0
|
go_router: ^14.4.1
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
home_widget: ^0.7.0
|
home_widget: ^0.7.0
|
||||||
hooks_riverpod: ^2.6.1
|
hooks_riverpod: ^2.6.1
|
||||||
html: ^0.15.4
|
html: ^0.15.5
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
lexo_rank:
|
lexo_rank:
|
||||||
@@ -41,7 +41,7 @@ dependencies:
|
|||||||
mime: ^2.0.0
|
mime: ^2.0.0
|
||||||
package_info_plus: ^8.1.0
|
package_info_plus: ^8.1.0
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.5
|
||||||
reorderable_grid: ^1.0.10
|
reorderable_grid: ^1.0.10
|
||||||
riverpod: ^2.6.1
|
riverpod: ^2.6.1
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
@@ -69,7 +69,7 @@ dev_dependencies:
|
|||||||
copy_with_extension_gen: ^5.0.4
|
copy_with_extension_gen: ^5.0.4
|
||||||
custom_lint: ^0.6.8
|
custom_lint: ^0.6.8
|
||||||
dependency_validator: ^4.1.1
|
dependency_validator: ^4.1.1
|
||||||
drift_dev: ^2.21.1
|
drift_dev: ^2.21.2
|
||||||
fast_equatable_lint: ^0.3.0
|
fast_equatable_lint: ^0.3.0
|
||||||
flutter_launcher_icons: ^0.14.1
|
flutter_launcher_icons: ^0.14.1
|
||||||
go_router_builder: ^2.7.1
|
go_router_builder: ^2.7.1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version = "1.0-SNAPSHOT"
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.9.22"
|
ext.kotlin_version = "1.9.22"
|
||||||
ext.mozillaComponentsVersion = '133.0.20241024211615'
|
ext.mozillaComponentsVersion = '134.0.20241030214633'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -74,48 +74,6 @@ android {
|
|||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
|
|
||||||
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
|
|
||||||
|
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
|
||||||
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
|
|
||||||
implementation 'androidx.activity:activity:1.9.3'
|
|
||||||
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.mockito:mockito-core:5.1.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.all {
|
unitTests.all {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
@@ -129,6 +87,66 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
configurations.configureEach {
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
exclude group: "org.mozilla.telemetry", module: "glean-native"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the Glean from GeckoView.
|
||||||
|
// `service-sync-logins` requires Glean, which pulls in glean-native,
|
||||||
|
// but that's also provided by geckoview-omni, so now we need to select which one to use.
|
||||||
|
project.configurations.configureEach {
|
||||||
|
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
|
||||||
|
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
|
||||||
|
if (toBeSelected != null) {
|
||||||
|
select(toBeSelected)
|
||||||
|
}
|
||||||
|
because 'use GeckoView Glean instead of standalone Glean'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:concept-toolbar:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:concept-awesomebar:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-storage-sync:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
|
||||||
|
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
|
||||||
|
|
||||||
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
||||||
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.9.3'
|
||||||
|
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
|
testImplementation("org.mockito:mockito-core:5.2.0")
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
const port = browser.runtime.connectNative("mozacReaderExtract");
|
||||||
|
|
||||||
|
let domChanged = false;
|
||||||
|
let lastContent = '';
|
||||||
|
|
||||||
|
function parseContent() {
|
||||||
|
const article = window.parseReaderable(document);
|
||||||
|
|
||||||
|
if (article && lastContent !== article.textContent) {
|
||||||
|
port.postMessage(article);
|
||||||
|
|
||||||
|
lastContent = article.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
domChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
port.onMessage.addListener((message) => {
|
||||||
|
if (message.action === "parseContent") {
|
||||||
|
if (domChanged) {
|
||||||
|
parseContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
domChanged = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const observerConfig = {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
attributes: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for page to be fully loaded
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
parseContent();
|
||||||
|
observer.observe(document.body, observerConfig);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', parseContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('unload', () => {
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "readability-extract@movenext.me"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Extract text of Websites",
|
||||||
|
"version": "1.0",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": [
|
||||||
|
"readability.min.js",
|
||||||
|
"content.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"mozillaAddons",
|
||||||
|
"geckoViewAddons",
|
||||||
|
"nativeMessaging",
|
||||||
|
"nativeMessagingFromContent",
|
||||||
|
"storage",
|
||||||
|
"tabs",
|
||||||
|
"webNavigation",
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../javascript/readability/dist/readability.min.js
|
||||||
+41
-10
@@ -15,7 +15,9 @@ import androidx.fragment.app.Fragment
|
|||||||
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
|
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
|
||||||
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
||||||
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
||||||
|
import eu.lensai.flutter_mozilla_components.pip.PictureInPictureIntegration
|
||||||
import eu.lensai.flutter_mozilla_components.services.DownloadService
|
import eu.lensai.flutter_mozilla_components.services.DownloadService
|
||||||
|
import mozilla.components.browser.state.selector.selectedTab
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
import mozilla.components.feature.app.links.AppLinksFeature
|
import mozilla.components.feature.app.links.AppLinksFeature
|
||||||
import mozilla.components.feature.downloads.DownloadsFeature
|
import mozilla.components.feature.downloads.DownloadsFeature
|
||||||
@@ -51,6 +53,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
||||||
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||||
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
|
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
|
||||||
|
private val pictureInPictureIntegration = ViewBoundFeatureWrapper<PictureInPictureIntegration>()
|
||||||
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
||||||
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
|
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
|
||||||
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
|
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
|
||||||
@@ -251,16 +254,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
|
|
||||||
fullScreenFeature.set(
|
fullScreenFeature.set(
|
||||||
feature = FullScreenFeature(
|
feature = FullScreenFeature(
|
||||||
components.core.store,
|
store = components.core.store,
|
||||||
components.useCases.sessionUseCases,
|
sessionUseCases = components.useCases.sessionUseCases,
|
||||||
sessionId,
|
tabId = sessionId,
|
||||||
) { inFullScreen ->
|
fullScreenChanged = ::fullScreenChanged,
|
||||||
if (inFullScreen) {
|
),
|
||||||
activity?.enterImmersiveMode()
|
|
||||||
} else {
|
|
||||||
activity?.exitImmersiveMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
owner = this,
|
owner = this,
|
||||||
view = binding.root,
|
view = binding.root,
|
||||||
)
|
)
|
||||||
@@ -275,6 +273,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
view = binding.root,
|
view = binding.root,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pictureInPictureIntegration.set(
|
||||||
|
feature = PictureInPictureIntegration(
|
||||||
|
components.core.store,
|
||||||
|
requireActivity(),
|
||||||
|
sessionId,
|
||||||
|
),
|
||||||
|
owner = this,
|
||||||
|
view = view,
|
||||||
|
)
|
||||||
|
|
||||||
secureWindowFeature.set(
|
secureWindowFeature.set(
|
||||||
feature = SecureWindowFeature(
|
feature = SecureWindowFeature(
|
||||||
window = requireActivity().window,
|
window = requireActivity().window,
|
||||||
@@ -312,11 +320,34 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fullScreenChanged(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
activity?.enterImmersiveMode()
|
||||||
|
} else {
|
||||||
|
activity?.exitImmersiveMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(): Boolean {
|
||||||
return backButtonHandler.any { it.onBackPressed() }
|
return backButtonHandler.any { it.onBackPressed() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final override fun onPause() {
|
||||||
|
pictureInPictureIntegration.get()?.onHomePressed() ?: false
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onPictureInPictureModeChanged(enabled: Boolean) {
|
||||||
|
val session = components.core.store.state.selectedTab
|
||||||
|
val fullScreenMode = session?.content?.fullScreen ?: false
|
||||||
|
// If we're exiting PIP mode and we're in fullscreen mode, then we should exit fullscreen mode as well.
|
||||||
|
if (!enabled && fullScreenMode) {
|
||||||
|
onBackPressed()
|
||||||
|
fullScreenChanged(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
|
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
|
||||||
return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
|
return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
|
||||||
|
|||||||
+13
-3
@@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
|
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
|
||||||
|
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
|
||||||
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||||
import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration
|
import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration
|
||||||
import mozilla.components.browser.state.state.WebExtensionState
|
import mozilla.components.browser.state.state.WebExtensionState
|
||||||
@@ -19,15 +20,16 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver
|
|||||||
/**
|
/**
|
||||||
* Fragment used for browsing the web within the main app.
|
* Fragment used for browsing the web within the main app.
|
||||||
*/
|
*/
|
||||||
class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler {
|
class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
|
||||||
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
|
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
|
||||||
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
|
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
|
||||||
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
|
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
|
||||||
|
private val readabilityExtractFeature = ViewBoundFeatureWrapper<ReadabilityExtractFeature>()
|
||||||
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
|
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
|
||||||
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
||||||
|
|
||||||
override fun createEngine(components: Components): EngineView {
|
override fun createEngine(components: Components): EngineView {
|
||||||
return components.core.engine.createView(context).apply {
|
return components.core.engine.createView(requireContext()).apply {
|
||||||
selectionActionDelegate = components.selectionAction
|
selectionActionDelegate = components.selectionAction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,12 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
|
|||||||
view = view,
|
view = view,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readabilityExtractFeature.set(
|
||||||
|
feature = components.features.readabilityExtractFeature,
|
||||||
|
owner = this,
|
||||||
|
view = view,
|
||||||
|
)
|
||||||
|
|
||||||
windowFeature.set(
|
windowFeature.set(
|
||||||
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
|
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
|
||||||
owner = this,
|
owner = this,
|
||||||
@@ -72,6 +80,8 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
|
|||||||
owner = this,
|
owner = this,
|
||||||
view = view,
|
view = view,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
components.core.historyStorage.registerStorageMaintenanceWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openPopup(webExtensionState: WebExtensionState) {
|
private fun openPopup(webExtensionState: WebExtensionState) {
|
||||||
@@ -86,7 +96,7 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
|
|||||||
readerViewFeature.onBackPressed() || super.onBackPressed()
|
readerViewFeature.onBackPressed() || super.onBackPressed()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply {
|
fun create(sessionId: String? = null) = BrowserFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putSessionId(sessionId)
|
putSessionId(sessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -5,10 +5,13 @@ import androidx.core.app.NotificationManagerCompat
|
|||||||
import eu.lensai.flutter_mozilla_components.components.Core
|
import eu.lensai.flutter_mozilla_components.components.Core
|
||||||
import eu.lensai.flutter_mozilla_components.components.Events
|
import eu.lensai.flutter_mozilla_components.components.Events
|
||||||
import eu.lensai.flutter_mozilla_components.components.Features
|
import eu.lensai.flutter_mozilla_components.components.Features
|
||||||
|
import eu.lensai.flutter_mozilla_components.components.Search
|
||||||
import eu.lensai.flutter_mozilla_components.components.Services
|
import eu.lensai.flutter_mozilla_components.components.Services
|
||||||
import eu.lensai.flutter_mozilla_components.components.UseCases
|
import eu.lensai.flutter_mozilla_components.components.UseCases
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
@@ -18,13 +21,15 @@ class Components(private val context: Context,
|
|||||||
val flutterEvents: GeckoStateEvents,
|
val flutterEvents: GeckoStateEvents,
|
||||||
val readerViewController: ReaderViewController,
|
val readerViewController: ReaderViewController,
|
||||||
val selectionAction: SelectionActionDelegate,
|
val selectionAction: SelectionActionDelegate,
|
||||||
val addonEvents: GeckoAddonEvents
|
val addonEvents: GeckoAddonEvents,
|
||||||
|
val tabContentEvents: GeckoTabContentEvents
|
||||||
) {
|
) {
|
||||||
val core by lazy { Core(context, this, flutterEvents) }
|
val core by lazy { Core(context, this, flutterEvents) }
|
||||||
val events by lazy { Events(flutterEvents) }
|
val events by lazy { Events(flutterEvents) }
|
||||||
val useCases by lazy { UseCases(context, core.engine, core.store) }
|
val useCases by lazy { UseCases(context, core.engine, core.store) }
|
||||||
val services by lazy { Services(context, useCases.tabsUseCases) }
|
val services by lazy { Services(context, useCases.tabsUseCases) }
|
||||||
val features by lazy { Features(core.store, addonEvents) }
|
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
|
||||||
|
val search by lazy { Search(context, core, useCases) }
|
||||||
|
|
||||||
var engineView: EngineView? = null
|
var engineView: EngineView? = null
|
||||||
var engineReportedInitialized = false
|
var engineReportedInitialized = false
|
||||||
|
|||||||
+2
@@ -32,6 +32,8 @@ object EngineProvider {
|
|||||||
// About config it's no longer enabled by default
|
// About config it's no longer enabled by default
|
||||||
builder.aboutConfigEnabled(true)
|
builder.aboutConfigEnabled(true)
|
||||||
builder.extensionsWebAPIEnabled(true)
|
builder.extensionsWebAPIEnabled(true)
|
||||||
|
builder.consoleOutput(true)
|
||||||
|
|
||||||
runtime = GeckoRuntime.create(context, builder.build())
|
runtime = GeckoRuntime.create(context, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -12,6 +12,7 @@ import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
|
|||||||
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
|
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
|
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
|
||||||
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
|
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.GeckoTabsApiImpl
|
||||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
@@ -25,6 +26,9 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionControll
|
|||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
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.GeckoTabsApi
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
||||||
@@ -63,13 +67,18 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
|||||||
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
val addonEvents = GeckoAddonEvents(_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(
|
GlobalComponents.setUp(
|
||||||
flutterPluginBinding.applicationContext,
|
flutterPluginBinding.applicationContext,
|
||||||
_flutterEvents,
|
_flutterEvents,
|
||||||
readerViewController,
|
readerViewController,
|
||||||
selectionActionDelegate,
|
selectionActionDelegate,
|
||||||
addonEvents
|
addonEvents,
|
||||||
|
tabContentEvents,
|
||||||
)
|
)
|
||||||
|
|
||||||
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
|
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
|
||||||
@@ -104,7 +113,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace this with your actual Fragment
|
// Replace this with your actual Fragment
|
||||||
val nativeFragment = BrowserFragment.create(activity!!)
|
val nativeFragment = BrowserFragment.create()
|
||||||
|
|
||||||
val fm = (activity as FragmentActivity).supportFragmentManager
|
val fm = (activity as FragmentActivity).supportFragmentManager
|
||||||
fm.beginTransaction()
|
fm.beginTransaction()
|
||||||
|
|||||||
+8
-1
@@ -3,11 +3,14 @@ package eu.lensai.flutter_mozilla_components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
||||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||||
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
||||||
import mozilla.components.support.base.facts.Facts
|
import mozilla.components.support.base.facts.Facts
|
||||||
@@ -40,7 +43,8 @@ object GlobalComponents {
|
|||||||
flutterEvents: GeckoStateEvents,
|
flutterEvents: GeckoStateEvents,
|
||||||
readerViewController: ReaderViewController,
|
readerViewController: ReaderViewController,
|
||||||
selectionAction: SelectionActionDelegate,
|
selectionAction: SelectionActionDelegate,
|
||||||
addonEvents: GeckoAddonEvents
|
addonEvents: GeckoAddonEvents,
|
||||||
|
tabContentEvents: GeckoTabContentEvents
|
||||||
) {
|
) {
|
||||||
Logger.debug("Creating new components")
|
Logger.debug("Creating new components")
|
||||||
|
|
||||||
@@ -50,6 +54,7 @@ object GlobalComponents {
|
|||||||
readerViewController,
|
readerViewController,
|
||||||
selectionAction,
|
selectionAction,
|
||||||
addonEvents,
|
addonEvents,
|
||||||
|
tabContentEvents
|
||||||
)
|
)
|
||||||
|
|
||||||
//newComponents.crashReporter.install(applicationContext)
|
//newComponents.crashReporter.install(applicationContext)
|
||||||
@@ -65,6 +70,8 @@ object GlobalComponents {
|
|||||||
//newComponents.useCases.downloadsUseCases.restoreDownloads()
|
//newComponents.useCases.downloadsUseCases.restoreDownloads()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
|
||||||
|
|
||||||
GlobalAddonDependencyProvider.initialize(
|
GlobalAddonDependencyProvider.initialize(
|
||||||
newComponents.core.addonManager,
|
newComponents.core.addonManager,
|
||||||
newComponents.core.addonUpdater,
|
newComponents.core.addonUpdater,
|
||||||
|
|||||||
-1
@@ -22,7 +22,6 @@ import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as Pigeon
|
|||||||
class GeckoSessionApiImpl : GeckoSessionApi {
|
class GeckoSessionApiImpl : GeckoSessionApi {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "GeckoSessionApi"
|
private const val TAG = "GeckoSessionApi"
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val components by lazy {
|
private val components by lazy {
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package eu.lensai.flutter_mozilla_components.api
|
||||||
|
|
||||||
|
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||||
|
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestion
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionType
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||||
|
|
||||||
|
class GeckoSuggestionApiImpl(
|
||||||
|
private val suggestionEvents: GeckoSuggestionEvents
|
||||||
|
) : GeckoSuggestionApi {
|
||||||
|
companion object {
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val components by lazy {
|
||||||
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInputChanged(text: String, providers: List<GeckoSuggestionType>) {
|
||||||
|
for(provider in providers) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val results = when(provider) {
|
||||||
|
GeckoSuggestionType.SESSION -> components.search.sessionSuggestions.onInputChanged(text)
|
||||||
|
GeckoSuggestionType.CLIPBOARD -> components.search.clipboardSuggestions.onInputChanged(text)
|
||||||
|
GeckoSuggestionType.HISTORY -> components.search.historySuggestions.onInputChanged(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mappedResults = results.map { result ->
|
||||||
|
GeckoSuggestion(
|
||||||
|
id = result.id,
|
||||||
|
title = result.title,
|
||||||
|
score = result.score.toLong(),
|
||||||
|
description = result.description,
|
||||||
|
type = provider,
|
||||||
|
editSuggestion = result.editSuggestion,
|
||||||
|
icon = result.icon?.toWebPBytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread {
|
||||||
|
suggestionEvents.onSuggestionResult(
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
provider,
|
||||||
|
mappedResults
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-1
@@ -20,6 +20,7 @@ import mozilla.components.browser.session.storage.SessionStorage
|
|||||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||||
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
|
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||||
import mozilla.components.concept.engine.DefaultSettings
|
import mozilla.components.concept.engine.DefaultSettings
|
||||||
@@ -35,6 +36,7 @@ import mozilla.components.feature.media.MediaSessionFeature
|
|||||||
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
||||||
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
||||||
import mozilla.components.feature.readerview.ReaderViewMiddleware
|
import mozilla.components.feature.readerview.ReaderViewMiddleware
|
||||||
|
import mozilla.components.feature.session.HistoryDelegate
|
||||||
import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
||||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||||
@@ -51,12 +53,13 @@ class Core(private val context: Context,
|
|||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val engineSettings by lazy {
|
private val engineSettings by lazy {
|
||||||
DefaultSettings().apply {
|
DefaultSettings().apply {
|
||||||
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
||||||
requestInterceptor = AppRequestInterceptor(context)
|
requestInterceptor = AppRequestInterceptor(context)
|
||||||
remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false)
|
remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false)
|
||||||
testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false)
|
testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false)
|
||||||
|
historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
||||||
trackingProtectionPolicy = createTrackingProtectionPolicy(prefs)
|
trackingProtectionPolicy = createTrackingProtectionPolicy(prefs)
|
||||||
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
|
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
|
||||||
globalPrivacyControlEnabled = prefs.getBoolean(
|
globalPrivacyControlEnabled = prefs.getBoolean(
|
||||||
@@ -159,6 +162,17 @@ class Core(private val context: Context,
|
|||||||
SessionStorage(context, engine)
|
SessionStorage(context, engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The storage component to persist browsing history (with the exception of
|
||||||
|
* private sessions).
|
||||||
|
*/
|
||||||
|
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience accessor to the [PlacesHistoryStorage].
|
||||||
|
*/
|
||||||
|
val historyStorage by lazy { lazyHistoryStorage.value }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a [TrackingProtectionPolicy] based on current preferences.
|
* Constructs a [TrackingProtectionPolicy] based on current preferences.
|
||||||
*
|
*
|
||||||
|
|||||||
+14
-1
@@ -1,12 +1,17 @@
|
|||||||
package eu.lensai.flutter_mozilla_components.components
|
package eu.lensai.flutter_mozilla_components.components
|
||||||
|
|
||||||
|
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
|
||||||
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.Engine
|
||||||
|
|
||||||
class Features(
|
class Features(
|
||||||
|
private val engine: Engine,
|
||||||
private val store: BrowserStore,
|
private val store: BrowserStore,
|
||||||
private val addonEvents: GeckoAddonEvents
|
private val addonEvents: GeckoAddonEvents,
|
||||||
|
private var tabContentEvents: GeckoTabContentEvents
|
||||||
) {
|
) {
|
||||||
val webExtensionToolbarFeature by lazy {
|
val webExtensionToolbarFeature by lazy {
|
||||||
WebExtensionToolbarFeature(
|
WebExtensionToolbarFeature(
|
||||||
@@ -14,4 +19,12 @@ class Features(
|
|||||||
addonEvents
|
addonEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val readabilityExtractFeature by lazy {
|
||||||
|
ReadabilityExtractFeature(
|
||||||
|
engine,
|
||||||
|
store,
|
||||||
|
tabContentEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package eu.lensai.flutter_mozilla_components.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
|
||||||
|
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
|
||||||
|
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
|
||||||
|
|
||||||
|
class Search(
|
||||||
|
private val context: Context,
|
||||||
|
private val core: Core,
|
||||||
|
private val useCases: UseCases,
|
||||||
|
) {
|
||||||
|
val sessionSuggestions by lazy {
|
||||||
|
SessionSuggestionProvider(
|
||||||
|
context.resources,
|
||||||
|
core.store,
|
||||||
|
useCases.tabsUseCases.selectTab,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val clipboardSuggestions by lazy {
|
||||||
|
ClipboardSuggestionProvider(
|
||||||
|
context,
|
||||||
|
useCases.sessionUseCases.loadUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val historySuggestions by lazy {
|
||||||
|
HistoryStorageSuggestionProvider(
|
||||||
|
core.historyStorage,
|
||||||
|
useCases.sessionUseCases.loadUrl,
|
||||||
|
core.icons,
|
||||||
|
core.engine,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
package eu.lensai.flutter_mozilla_components.feature
|
||||||
|
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
|
import eu.lensai.flutter_mozilla_components.pigeons.TabContent
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.concept.engine.Engine
|
||||||
|
import mozilla.components.concept.engine.webextension.MessageHandler
|
||||||
|
import mozilla.components.concept.engine.webextension.Port
|
||||||
|
import mozilla.components.lib.state.ext.flowScoped
|
||||||
|
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
|
import mozilla.components.support.ktx.android.org.json.tryGetString
|
||||||
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
|
||||||
|
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||||
|
import mozilla.components.support.webextensions.WebExtensionController
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class ReadabilityExtractFeature(
|
||||||
|
private var engine: Engine,
|
||||||
|
private var store: BrowserStore,
|
||||||
|
private var tabContentEvents: GeckoTabContentEvents
|
||||||
|
): LifecycleAwareFeature {
|
||||||
|
companion object {
|
||||||
|
private val logger = Logger("ReadabilityExtract")
|
||||||
|
|
||||||
|
internal const val READER_EXTRACT_EXTENSION_ID = "readability-extract@movenext.me"
|
||||||
|
internal const val READER_EXTRACT_CONTENT_PORT = "mozacReaderExtract"
|
||||||
|
internal const val READER_EXTRACT_EXTENSION_URL =
|
||||||
|
"resource://android/assets/extensions/readability_extract/"
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContentMessageHandler(
|
||||||
|
private var tabContentEvents: GeckoTabContentEvents,
|
||||||
|
private var tabId: String
|
||||||
|
) : MessageHandler {
|
||||||
|
override fun onPortConnected(port: Port) {
|
||||||
|
logger.debug("Port connected: ${port.name()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPortMessage(message: Any, port: Port) {
|
||||||
|
if (message is JSONObject) {
|
||||||
|
tabContentEvents.onContentUpdate(
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
contentArg = TabContent(
|
||||||
|
tabId = tabId,
|
||||||
|
fullContentMarkdown = message.tryGetString("fullContentMarkdown"),
|
||||||
|
fullContentPlain = message.tryGetString("fullContentPlain"),
|
||||||
|
extractedContentMarkdown = message.tryGetString("extractedContentMarkdown"),
|
||||||
|
extractedContentPlain = message.tryGetString("extractedContentPlain")
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sessionScope: CoroutineScope? = null
|
||||||
|
private var historyScope: CoroutineScope? = null
|
||||||
|
|
||||||
|
private var extensionController = WebExtensionController(
|
||||||
|
READER_EXTRACT_EXTENSION_ID,
|
||||||
|
READER_EXTRACT_EXTENSION_URL,
|
||||||
|
READER_EXTRACT_CONTENT_PORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
ensureExtensionInstalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
sessionScope?.cancel()
|
||||||
|
historyScope?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureExtensionInstalled() {
|
||||||
|
extensionController.install(
|
||||||
|
engine,
|
||||||
|
onSuccess = { extension ->
|
||||||
|
sessionScope = store.flowScoped { flow ->
|
||||||
|
flow.map { it.tabs }
|
||||||
|
.filterChanged { it.engineState.engineSession }
|
||||||
|
.collect { state ->
|
||||||
|
val engineSession = state.engineState.engineSession ?: return@collect
|
||||||
|
|
||||||
|
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val handler = ContentMessageHandler(tabContentEvents, state.id)
|
||||||
|
extension.registerContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyScope = store.flowScoped { flow ->
|
||||||
|
flow.map { it.tabs }
|
||||||
|
.filterChanged { it.content.history }
|
||||||
|
.ifAnyChanged { arrayOf(it.content.history.currentIndex) }
|
||||||
|
.collect { state ->
|
||||||
|
val engineSession = state.engineState.engineSession ?: return@collect
|
||||||
|
|
||||||
|
if(state.content.loading) {
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
|
||||||
|
if(extensionController.portConnected(engineSession)) {
|
||||||
|
extensionController.sendContentMessage(
|
||||||
|
JSONObject().put("action", "parseContent"),
|
||||||
|
engineSession
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = { throwable ->
|
||||||
|
Logger.error("Could not install $READER_EXTRACT_EXTENSION_ID extension", throwable)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-50
@@ -119,7 +119,7 @@ class WebExtensionToolbarFeature(
|
|||||||
extension.browserAction?.let { browserAction ->
|
extension.browserAction?.let { browserAction ->
|
||||||
addOrUpdateAction(
|
addOrUpdateAction(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
defaultAction = browserAction,
|
globalAction = browserAction,
|
||||||
tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
|
tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ class WebExtensionToolbarFeature(
|
|||||||
if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
|
if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
|
||||||
addOrUpdateAction(
|
addOrUpdateAction(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
defaultAction = pageAction,
|
globalAction = pageAction,
|
||||||
tabAction = tabPageAction,
|
tabAction = tabPageAction,
|
||||||
isPageAction = true,
|
isPageAction = true,
|
||||||
)
|
)
|
||||||
@@ -156,62 +156,60 @@ class WebExtensionToolbarFeature(
|
|||||||
|
|
||||||
private fun addOrUpdateAction(
|
private fun addOrUpdateAction(
|
||||||
extension: WebExtensionState,
|
extension: WebExtensionState,
|
||||||
defaultAction: Action,
|
globalAction: Action,
|
||||||
tabAction: Action?,
|
tabAction: Action?,
|
||||||
isPageAction: Boolean = false,
|
isPageAction: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var action = defaultAction
|
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
|
||||||
|
// Add the global page/browser action if it doesn't exist
|
||||||
|
var toolbarAction = actionMap.getOrPut(extension.id) {
|
||||||
|
CoroutineScope(iconJobDispatcher).launch {
|
||||||
|
try {
|
||||||
|
//TODO: make variable size
|
||||||
|
val icon = globalAction.loadIcon?.invoke(128)
|
||||||
|
icon?.let {
|
||||||
|
val imageBytes = icon.toWebPBytes()
|
||||||
|
runOnUiThread {
|
||||||
|
addonEvents.onUpdateWebExtensionIcon(
|
||||||
|
timestampArg = System.currentTimeMillis(),
|
||||||
|
extensionIdArg = extension.id,
|
||||||
|
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
||||||
|
iconArg = imageBytes
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Log.log(
|
||||||
|
Log.Priority.ERROR,
|
||||||
|
"mozac-webextensions",
|
||||||
|
throwable,
|
||||||
|
"Failed to load browser action icon, falling back to default.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalAction
|
||||||
|
}
|
||||||
|
|
||||||
// Apply tab-specific override of browser/page action
|
// Apply tab-specific override of browser/page action
|
||||||
tabAction?.let {
|
tabAction?.let {
|
||||||
action = action.copyWithOverride(it)
|
toolbarAction = toolbarAction.copyWithOverride(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(iconJobDispatcher).launch {
|
val data = WebExtensionData(
|
||||||
try {
|
extensionId = extension.id,
|
||||||
//TODO: make variable size
|
title = toolbarAction.title,
|
||||||
val icon = action.loadIcon?.invoke(128)
|
enabled = toolbarAction.enabled,
|
||||||
icon?.let {
|
badgeText = toolbarAction.badgeText,
|
||||||
val imageBytes = icon.toWebPBytes()
|
badgeTextColor = toolbarAction.badgeTextColor?.toLong(),
|
||||||
runOnUiThread {
|
badgeBackgroundColor = toolbarAction.badgeBackgroundColor?.toLong(),
|
||||||
addonEvents.onUpdateWebExtensionIcon(
|
)
|
||||||
timestampArg = System.currentTimeMillis(),
|
|
||||||
extensionIdArg = extension.id,
|
|
||||||
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
|
||||||
iconArg = imageBytes
|
|
||||||
) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Log.log(
|
|
||||||
Log.Priority.ERROR,
|
|
||||||
"mozac-webextensions",
|
|
||||||
throwable,
|
|
||||||
"Failed to load browser action icon, falling back to default.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
|
addonEvents.onUpsertWebExtensionAction(
|
||||||
// Add the global page/browser action if it doesn't exist
|
timestampArg = System.currentTimeMillis(),
|
||||||
val toolbarAction = actionMap.getOrPut(extension.id) {
|
extensionIdArg = extension.id,
|
||||||
val toolbarAction = WebExtensionData(
|
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
||||||
extensionId = extension.id,
|
extensionDataArg = data
|
||||||
title = action.title,
|
) { }
|
||||||
enabled = action.enabled,
|
|
||||||
badgeText = action.badgeText,
|
|
||||||
badgeTextColor = action.badgeTextColor?.toLong(),
|
|
||||||
badgeBackgroundColor = action.badgeBackgroundColor?.toLong(),
|
|
||||||
)
|
|
||||||
|
|
||||||
addonEvents.onUpsertWebExtensionAction(
|
|
||||||
timestampArg = System.currentTimeMillis(),
|
|
||||||
extensionIdArg = extension.id,
|
|
||||||
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
|
||||||
extensionDataArg = toolbarAction
|
|
||||||
) { }
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -30,12 +30,12 @@ class ReaderViewIntegration(
|
|||||||
if (enabled) {
|
if (enabled) {
|
||||||
feature.showReaderView()
|
feature.showReaderView()
|
||||||
|
|
||||||
readerViewController.appearanceButtonVisibility(true) { _ -> };
|
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),true) { _ -> };
|
||||||
} else {
|
} else {
|
||||||
feature.hideReaderView()
|
feature.hideReaderView()
|
||||||
feature.hideControls()
|
feature.hideControls()
|
||||||
|
|
||||||
readerViewController.appearanceButtonVisibility(false) { _ -> };
|
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),false) { _ -> };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+245
-53
@@ -154,6 +154,18 @@ enum class WebExtensionActionType(val raw: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class GeckoSuggestionType(val raw: Int) {
|
||||||
|
SESSION(0),
|
||||||
|
CLIPBOARD(1),
|
||||||
|
HISTORY(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): GeckoSuggestionType? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translation options that map to the Gecko Translations Options.
|
* Translation options that map to the Gecko Translations Options.
|
||||||
*
|
*
|
||||||
@@ -968,6 +980,72 @@ data class WebExtensionData (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class GeckoSuggestion (
|
||||||
|
val id: String,
|
||||||
|
val type: GeckoSuggestionType,
|
||||||
|
val score: Long,
|
||||||
|
val title: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val editSuggestion: String? = null,
|
||||||
|
val icon: ByteArray? = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): GeckoSuggestion {
|
||||||
|
val id = pigeonVar_list[0] as String
|
||||||
|
val type = pigeonVar_list[1] as GeckoSuggestionType
|
||||||
|
val score = pigeonVar_list[2] as Long
|
||||||
|
val title = pigeonVar_list[3] as String?
|
||||||
|
val description = pigeonVar_list[4] as String?
|
||||||
|
val editSuggestion = pigeonVar_list[5] as String?
|
||||||
|
val icon = pigeonVar_list[6] as ByteArray?
|
||||||
|
return GeckoSuggestion(id, type, score, title, description, editSuggestion, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
score,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
editSuggestion,
|
||||||
|
icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class TabContent (
|
||||||
|
val tabId: String,
|
||||||
|
val fullContentMarkdown: String? = null,
|
||||||
|
val fullContentPlain: String? = null,
|
||||||
|
val extractedContentMarkdown: String? = null,
|
||||||
|
val extractedContentPlain: String? = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): TabContent {
|
||||||
|
val tabId = pigeonVar_list[0] as String
|
||||||
|
val fullContentMarkdown = pigeonVar_list[1] as String?
|
||||||
|
val fullContentPlain = pigeonVar_list[2] as String?
|
||||||
|
val extractedContentMarkdown = pigeonVar_list[3] as String?
|
||||||
|
val extractedContentPlain = pigeonVar_list[4] as String?
|
||||||
|
return TabContent(tabId, fullContentMarkdown, fullContentPlain, extractedContentMarkdown, extractedContentPlain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
tabId,
|
||||||
|
fullContentMarkdown,
|
||||||
|
fullContentPlain,
|
||||||
|
extractedContentMarkdown,
|
||||||
|
extractedContentPlain,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
private open class GeckoPigeonCodec : StandardMessageCodec() {
|
private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
@@ -1007,130 +1085,145 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
136.toByte() -> {
|
136.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as Long?)?.let {
|
||||||
TranslationOptions.fromList(it)
|
GeckoSuggestionType.ofRaw(it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
137.toByte() -> {
|
137.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
ReaderState.fromList(it)
|
TranslationOptions.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
138.toByte() -> {
|
138.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
LastMediaAccessState.fromList(it)
|
ReaderState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
139.toByte() -> {
|
139.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
HistoryMetadataKey.fromList(it)
|
LastMediaAccessState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
140.toByte() -> {
|
140.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
PackageCategoryValue.fromList(it)
|
HistoryMetadataKey.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
141.toByte() -> {
|
141.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
ExternalPackage.fromList(it)
|
PackageCategoryValue.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
142.toByte() -> {
|
142.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
LoadUrlFlagsValue.fromList(it)
|
ExternalPackage.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
143.toByte() -> {
|
143.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
SourceValue.fromList(it)
|
LoadUrlFlagsValue.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
144.toByte() -> {
|
144.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
TabState.fromList(it)
|
SourceValue.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
145.toByte() -> {
|
145.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
RecoverableTab.fromList(it)
|
TabState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
146.toByte() -> {
|
146.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
RecoverableBrowserState.fromList(it)
|
RecoverableTab.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
147.toByte() -> {
|
147.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
IconRequest.fromList(it)
|
RecoverableBrowserState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
148.toByte() -> {
|
148.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
ResourceSize.fromList(it)
|
IconRequest.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
149.toByte() -> {
|
149.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
Resource.fromList(it)
|
ResourceSize.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
150.toByte() -> {
|
150.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
IconResult.fromList(it)
|
Resource.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
151.toByte() -> {
|
151.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
CookiePartitionKey.fromList(it)
|
IconResult.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
152.toByte() -> {
|
152.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
Cookie.fromList(it)
|
CookiePartitionKey.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
153.toByte() -> {
|
153.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
HistoryItem.fromList(it)
|
Cookie.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
154.toByte() -> {
|
154.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
HistoryState.fromList(it)
|
HistoryItem.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
155.toByte() -> {
|
155.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
ReaderableState.fromList(it)
|
HistoryState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
156.toByte() -> {
|
156.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
SecurityInfoState.fromList(it)
|
ReaderableState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
157.toByte() -> {
|
157.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
TabContentState.fromList(it)
|
SecurityInfoState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
158.toByte() -> {
|
158.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
FindResultState.fromList(it)
|
TabContentState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
159.toByte() -> {
|
159.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
CustomSelectionAction.fromList(it)
|
FindResultState.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
160.toByte() -> {
|
160.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
CustomSelectionAction.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
161.toByte() -> {
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
WebExtensionData.fromList(it)
|
WebExtensionData.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
162.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
GeckoSuggestion.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
163.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
TabContent.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> super.readValueOfType(type, buffer)
|
else -> super.readValueOfType(type, buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1164,106 +1257,118 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
|||||||
stream.write(135)
|
stream.write(135)
|
||||||
writeValue(stream, value.raw)
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is TranslationOptions -> {
|
is GeckoSuggestionType -> {
|
||||||
stream.write(136)
|
stream.write(136)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.raw)
|
||||||
}
|
}
|
||||||
is ReaderState -> {
|
is TranslationOptions -> {
|
||||||
stream.write(137)
|
stream.write(137)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is LastMediaAccessState -> {
|
is ReaderState -> {
|
||||||
stream.write(138)
|
stream.write(138)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is HistoryMetadataKey -> {
|
is LastMediaAccessState -> {
|
||||||
stream.write(139)
|
stream.write(139)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is PackageCategoryValue -> {
|
is HistoryMetadataKey -> {
|
||||||
stream.write(140)
|
stream.write(140)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is ExternalPackage -> {
|
is PackageCategoryValue -> {
|
||||||
stream.write(141)
|
stream.write(141)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is LoadUrlFlagsValue -> {
|
is ExternalPackage -> {
|
||||||
stream.write(142)
|
stream.write(142)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is SourceValue -> {
|
is LoadUrlFlagsValue -> {
|
||||||
stream.write(143)
|
stream.write(143)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is TabState -> {
|
is SourceValue -> {
|
||||||
stream.write(144)
|
stream.write(144)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is RecoverableTab -> {
|
is TabState -> {
|
||||||
stream.write(145)
|
stream.write(145)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is RecoverableBrowserState -> {
|
is RecoverableTab -> {
|
||||||
stream.write(146)
|
stream.write(146)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is IconRequest -> {
|
is RecoverableBrowserState -> {
|
||||||
stream.write(147)
|
stream.write(147)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is ResourceSize -> {
|
is IconRequest -> {
|
||||||
stream.write(148)
|
stream.write(148)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is Resource -> {
|
is ResourceSize -> {
|
||||||
stream.write(149)
|
stream.write(149)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is IconResult -> {
|
is Resource -> {
|
||||||
stream.write(150)
|
stream.write(150)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is CookiePartitionKey -> {
|
is IconResult -> {
|
||||||
stream.write(151)
|
stream.write(151)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is Cookie -> {
|
is CookiePartitionKey -> {
|
||||||
stream.write(152)
|
stream.write(152)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is HistoryItem -> {
|
is Cookie -> {
|
||||||
stream.write(153)
|
stream.write(153)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is HistoryState -> {
|
is HistoryItem -> {
|
||||||
stream.write(154)
|
stream.write(154)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is ReaderableState -> {
|
is HistoryState -> {
|
||||||
stream.write(155)
|
stream.write(155)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is SecurityInfoState -> {
|
is ReaderableState -> {
|
||||||
stream.write(156)
|
stream.write(156)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is TabContentState -> {
|
is SecurityInfoState -> {
|
||||||
stream.write(157)
|
stream.write(157)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is FindResultState -> {
|
is TabContentState -> {
|
||||||
stream.write(158)
|
stream.write(158)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is CustomSelectionAction -> {
|
is FindResultState -> {
|
||||||
stream.write(159)
|
stream.write(159)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is WebExtensionData -> {
|
is CustomSelectionAction -> {
|
||||||
stream.write(160)
|
stream.write(160)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
|
is WebExtensionData -> {
|
||||||
|
stream.write(161)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is GeckoSuggestion -> {
|
||||||
|
stream.write(162)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is TabContent -> {
|
||||||
|
stream.write(163)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
else -> super.writeValue(stream, value)
|
else -> super.writeValue(stream, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2553,12 +2658,12 @@ class ReaderViewController(private val binaryMessenger: BinaryMessenger, private
|
|||||||
GeckoPigeonCodec()
|
GeckoPigeonCodec()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun appearanceButtonVisibility(visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
|
fun appearanceButtonVisibility(timestampArg: Long, visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
|
||||||
{
|
{
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$separatedMessageChannelSuffix"
|
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$separatedMessageChannelSuffix"
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
channel.send(listOf(visibleArg)) {
|
channel.send(listOf(timestampArg, visibleArg)) {
|
||||||
if (it is List<*>) {
|
if (it is List<*>) {
|
||||||
if (it.size > 1) {
|
if (it.size > 1) {
|
||||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
@@ -2743,3 +2848,90 @@ class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface GeckoSuggestionApi {
|
||||||
|
fun onInputChanged(text: String, providers: List<GeckoSuggestionType>)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by GeckoSuggestionApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
GeckoPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `GeckoSuggestionApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoSuggestionApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val textArg = args[0] as String
|
||||||
|
val providersArg = args[1] as List<GeckoSuggestionType>
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.onInputChanged(textArg, providersArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||||
|
class GeckoSuggestionEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||||
|
companion object {
|
||||||
|
/** The codec used by GeckoSuggestionEvents. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
GeckoPigeonCodec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onSuggestionResult(timestampArg: Long, suggestionTypeArg: GeckoSuggestionType, suggestionsArg: List<GeckoSuggestion>, callback: (Result<Unit>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(timestampArg, suggestionTypeArg, suggestionsArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||||
|
class GeckoTabContentEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||||
|
companion object {
|
||||||
|
/** The codec used by GeckoTabContentEvents. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
GeckoPigeonCodec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onContentUpdate(timestampArg: Long, contentArg: TabContent, callback: (Result<Unit>) -> Unit)
|
||||||
|
{
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$separatedMessageChannelSuffix"
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||||
|
channel.send(listOf(timestampArg, contentArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(createConnectionError(channelName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package eu.lensai.flutter_mozilla_components.pip
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
|
||||||
|
import mozilla.components.browser.state.store.BrowserStore
|
||||||
|
import mozilla.components.feature.session.PictureInPictureFeature
|
||||||
|
import mozilla.components.lib.state.ext.flowScoped
|
||||||
|
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||||
|
|
||||||
|
class PictureInPictureIntegration(
|
||||||
|
private val store: BrowserStore,
|
||||||
|
activity: Activity,
|
||||||
|
private val customTabId: String?,
|
||||||
|
private val whiteList: List<String> = listOf("youtube.com/tv"),
|
||||||
|
) : LifecycleAwareFeature {
|
||||||
|
private var scope: CoroutineScope? = null
|
||||||
|
private val pictureFeature = PictureInPictureFeature(store, activity)
|
||||||
|
private var whiteListed = false
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
scope = store.flowScoped { flow ->
|
||||||
|
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabId) }
|
||||||
|
.distinctUntilChangedBy { it.content.url }
|
||||||
|
.collect { whiteListed = isWhitelisted(it.content.url) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
scope?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHomePressed() = if (whiteListed) {
|
||||||
|
pictureFeature.enterPipModeCompat()
|
||||||
|
} else {
|
||||||
|
pictureFeature.onHomePressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWhitelisted(url: String): Boolean {
|
||||||
|
val exists = whiteList.firstOrNull { url.contains(it) }
|
||||||
|
return exists != null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>readability [9 Jul 2024 at 10:42]</title>
|
<title>readability [6 Nov 2024 at 17:38]</title>
|
||||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABrVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+O1foceMD///+J0/qK1Pr7/v8Xdr/9///W8P4UdL7L7P0Scr2r4Pyj3vwad8D5/f/2/f+55f3E6f34+/2H0/ojfMKpzOd0rNgQcb3F3O/j9f7c8v6g3Pz0/P/w+v/q+P7n9v6T1/uQ1vuE0vqLut/y+v+Z2fvt+f+15Pzv9fuc2/vR7v2V2Pvd6/bg9P7I6/285/2y4/yp3/zp8vk8i8kqgMT7/P31+fyv4vxGkcz6/P6/6P3j7vfS5PNnpNUxhcbO7f7F6v3O4vHK3/DA2u631Ouy0eqXweKJud5wqthfoNMMbLvY8f73+v2dxeR8sNtTmdDx9/zX6PSjyeaCtd1YnNGX2PuQveCGt95Nls42h8dLlM3F4vBtAAAAM3RSTlMAAyOx0/sKBvik8opWGBMOAe3l1snDm2E9LSb06eHcu5JpHbarfHZCN9CBb08zzkdNS0kYaptYAAAFV0lEQVRYw92X51/aYBDHHS2O2qqttVbrqNq9m+TJIAYIShBkWwqIiCgoWvfeq7Z2/s29hyQNyUcR7LveGwVyXy6XH8/9rqxglLfUPLxVduUor3h0rfp2TYvpivk37929TkG037hffoX0+peVtZQc1589rigVUdXS/ABSAyEmGIO/1XfvldSK8vs3OqB6u3m0nxmIrvgB0dj7rr7Y9IbuF68hnfFaiHA/sxqm0wciIG43P60qKv9WXWc1RXGh/mFESFABTSBi0sNAKzqet17eCtOb3kZIDwxEEU0oAIJGYxNBDhBND29e0rtXXbcpuPmED9IhEAAQ/AXEaF8EPmnrrKsv0LvWR3fg5sWDNAFZOgAgaKvZDogHNU9MFwnnYROkc56RD5CjAbQX9Ow4g7upCsvYu55aSI/Nj0H1akgKQEUM94dwK65hYRmFU9MIcH/fqJYOZYcnuJSU/waKDgTOEVaVKhwrTRP5XzgSpAITYzom7UvkhFX5VutmxeNnWDjjswTKTyfgluNDGbUpWissXhF3s7mlSml+czWkg3D0l1nNjGNjz3myOQOa1KM/jOS6ebdbAVTCi4gljHSFrviza7tOgRWcS0MOUX9zdNgag5w7rRqA44Lzw0hr1WqES36dFliSJFlh2rXIae3FFcDDgKdxrUIDePr8jGcSClV1u7A9xeN0ModY/pHMxmR1EzRh8TJiwqsHmKW0l4FCEZI+jHio+JdPPE9qwQtTRxku2D8sIeRL2LnxWSllANCQGOIiqVHAz2ye2JR0DcH+HoxDkaADLjgxjKQ+AwCX/g0+DNgdG0ukYCONAe+dbc2IAc6fwt1ARoDSezNHxV2Cmzwv3O6lDMV55edBGwGK9n1+x2F8EDfAGCxug8MhpsMEcTEAWf3rx2vZhe/LAmtIn/6apE6PN0ULKgywD9mmdxbmFl3OvD5AS5fW5zLbv/YHmcsBTjf/afDz3MaZTVCfAP9z6/Bw6ycv8EUBWJIn9zYcoAWWlW9+OzO3vkTy8H+RANLmdrpOuYWdZYEXpo+TlCJrW5EARb7fF+bWdqf3hhyZI1nWJQHgznErZhbjoEsWqi8dQNoE294aldzFurwSABL2XXMf9+H1VQGke9exw5P/AnA5Pv5ngMul7LOvO922iwACu8WkCwLCafvM4CeWPxfA8lNHcWZSoi8EwMAIciKX2Z4SWCMAa3snCZ/G4EA8D6CMLNFsGQhkkz/gQNEBbPCbWsxGUpYVu3z8IyNAknwJkfPMEhLyrdi5RTyUVACkw4GSFRNWJNEW+fgPGwHD8/JxnRuLabN4CGNRkAE23na2+VmEAUmrYymSGjMAYqH84YUIyzgzs3XC7gNgH36Vcc4zKY9o9fgPBXUAiHHwVboBHGLiX6Zcjp1f2wu4tvzZKo0ecPnDtQYDQvJXaBeNzce45Fp28ZQLrEZVuFqgBwOalArKXnW1UzlnSusQKJqKYNuz4tOnI6sZG4zanpemv+7ySU2jbA9h6uhcgpfy6G2PahirDZ6zvq6zDduMVFTKvzw8wgyEdelwY9in3XkEPs3osJuwRQ4qTkfzifndg9Gfc4pdsu82+tTnHZTBa2EAMrqr2t43pguc8tNm7JQVQ2S0ukj2d22dhXYP0/veWtwKrCkNoNimAN5+Xr/oLrxswKbVJjteWrX7eR63o4j9q0GxnaBdWgGA5VStpanIjQmEhV0/nVt5VOFUvix6awJhPcAaTEShgrG+iGyvb5a0Ndb1YGHFPEwoqAinoaykaID1o1pdPNu7XsnCKQ3R+hwWIIhGvORcJUBYXe3Xa3vq/mF/N9V13ugufMkfXn+KHsRD0B8AAAAASUVORK5CYII=" type="image/x-icon" />
|
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABrVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+O1foceMD///+J0/qK1Pr7/v8Xdr/9///W8P4UdL7L7P0Scr2r4Pyj3vwad8D5/f/2/f+55f3E6f34+/2H0/ojfMKpzOd0rNgQcb3F3O/j9f7c8v6g3Pz0/P/w+v/q+P7n9v6T1/uQ1vuE0vqLut/y+v+Z2fvt+f+15Pzv9fuc2/vR7v2V2Pvd6/bg9P7I6/285/2y4/yp3/zp8vk8i8kqgMT7/P31+fyv4vxGkcz6/P6/6P3j7vfS5PNnpNUxhcbO7f7F6v3O4vHK3/DA2u631Ouy0eqXweKJud5wqthfoNMMbLvY8f73+v2dxeR8sNtTmdDx9/zX6PSjyeaCtd1YnNGX2PuQveCGt95Nls42h8dLlM3F4vBtAAAAM3RSTlMAAyOx0/sKBvik8opWGBMOAe3l1snDm2E9LSb06eHcu5JpHbarfHZCN9CBb08zzkdNS0kYaptYAAAFV0lEQVRYw92X51/aYBDHHS2O2qqttVbrqNq9m+TJIAYIShBkWwqIiCgoWvfeq7Z2/s29hyQNyUcR7LveGwVyXy6XH8/9rqxglLfUPLxVduUor3h0rfp2TYvpivk37929TkG037hffoX0+peVtZQc1589rigVUdXS/ABSAyEmGIO/1XfvldSK8vs3OqB6u3m0nxmIrvgB0dj7rr7Y9IbuF68hnfFaiHA/sxqm0wciIG43P60qKv9WXWc1RXGh/mFESFABTSBi0sNAKzqet17eCtOb3kZIDwxEEU0oAIJGYxNBDhBND29e0rtXXbcpuPmED9IhEAAQ/AXEaF8EPmnrrKsv0LvWR3fg5sWDNAFZOgAgaKvZDogHNU9MFwnnYROkc56RD5CjAbQX9Ow4g7upCsvYu55aSI/Nj0H1akgKQEUM94dwK65hYRmFU9MIcH/fqJYOZYcnuJSU/waKDgTOEVaVKhwrTRP5XzgSpAITYzom7UvkhFX5VutmxeNnWDjjswTKTyfgluNDGbUpWissXhF3s7mlSml+czWkg3D0l1nNjGNjz3myOQOa1KM/jOS6ebdbAVTCi4gljHSFrviza7tOgRWcS0MOUX9zdNgag5w7rRqA44Lzw0hr1WqES36dFliSJFlh2rXIae3FFcDDgKdxrUIDePr8jGcSClV1u7A9xeN0ModY/pHMxmR1EzRh8TJiwqsHmKW0l4FCEZI+jHio+JdPPE9qwQtTRxku2D8sIeRL2LnxWSllANCQGOIiqVHAz2ye2JR0DcH+HoxDkaADLjgxjKQ+AwCX/g0+DNgdG0ukYCONAe+dbc2IAc6fwt1ARoDSezNHxV2Cmzwv3O6lDMV55edBGwGK9n1+x2F8EDfAGCxug8MhpsMEcTEAWf3rx2vZhe/LAmtIn/6apE6PN0ULKgywD9mmdxbmFl3OvD5AS5fW5zLbv/YHmcsBTjf/afDz3MaZTVCfAP9z6/Bw6ycv8EUBWJIn9zYcoAWWlW9+OzO3vkTy8H+RANLmdrpOuYWdZYEXpo+TlCJrW5EARb7fF+bWdqf3hhyZI1nWJQHgznErZhbjoEsWqi8dQNoE294aldzFurwSABL2XXMf9+H1VQGke9exw5P/AnA5Pv5ngMul7LOvO922iwACu8WkCwLCafvM4CeWPxfA8lNHcWZSoi8EwMAIciKX2Z4SWCMAa3snCZ/G4EA8D6CMLNFsGQhkkz/gQNEBbPCbWsxGUpYVu3z8IyNAknwJkfPMEhLyrdi5RTyUVACkw4GSFRNWJNEW+fgPGwHD8/JxnRuLabN4CGNRkAE23na2+VmEAUmrYymSGjMAYqH84YUIyzgzs3XC7gNgH36Vcc4zKY9o9fgPBXUAiHHwVboBHGLiX6Zcjp1f2wu4tvzZKo0ecPnDtQYDQvJXaBeNzce45Fp28ZQLrEZVuFqgBwOalArKXnW1UzlnSusQKJqKYNuz4tOnI6sZG4zanpemv+7ySU2jbA9h6uhcgpfy6G2PahirDZ6zvq6zDduMVFTKvzw8wgyEdelwY9in3XkEPs3osJuwRQ4qTkfzifndg9Gfc4pdsu82+tTnHZTBa2EAMrqr2t43pguc8tNm7JQVQ2S0ukj2d22dhXYP0/veWtwKrCkNoNimAN5+Xr/oLrxswKbVJjteWrX7eR63o4j9q0GxnaBdWgGA5VStpanIjQmEhV0/nVt5VOFUvix6awJhPcAaTEShgrG+iGyvb5a0Ndb1YGHFPEwoqAinoaykaID1o1pdPNu7XsnCKQ3R+hwWIIhGvORcJUBYXe3Xa3vq/mF/N9V13ugufMkfXn+KHsRD0B8AAAAASUVORK5CYII=" type="image/x-icon" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script>
|
<script>
|
||||||
window.chartData = [{"label":"readability.min.js","isAsset":true,"statSize":156970,"parsedSize":55954,"gzipSize":18947,"groups":[{"label":"node_modules","path":"./node_modules","statSize":155558,"groups":[{"label":"@mozilla/readability","path":"./node_modules/@mozilla/readability","statSize":88421,"groups":[{"id":804,"label":"Readability-readerable.js","path":"./node_modules/@mozilla/readability/Readability-readerable.js","statSize":4162},{"id":238,"label":"Readability.js","path":"./node_modules/@mozilla/readability/Readability.js","statSize":84033},{"id":396,"label":"index.js","path":"./node_modules/@mozilla/readability/index.js","statSize":226}],"parsedSize":0,"gzipSize":0},{"label":"dompurify/dist","path":"./node_modules/dompurify/dist","statSize":67137,"groups":[{"id":838,"label":"purify.js","path":"./node_modules/dompurify/dist/purify.js","statSize":67137}],"parsedSize":0,"gzipSize":0}],"parsedSize":0,"gzipSize":0},{"label":"src","path":"./src","statSize":1412,"groups":[{"id":954,"label":"index.js","path":"./src/index.js","statSize":1412,"parsedSize":55946,"gzipSize":18947}],"parsedSize":55946,"gzipSize":18947}],"isInitialByEntrypoint":{"main":true}}];
|
window.chartData = [{"label":"readability.min.js","isAsset":true,"statSize":120090,"parsedSize":48006,"gzipSize":15433,"groups":[{"label":"node_modules","path":"./node_modules","statSize":92482,"groups":[{"label":"@mozilla/readability","path":"./node_modules/@mozilla/readability","statSize":88421,"groups":[{"id":804,"label":"Readability-readerable.js","path":"./node_modules/@mozilla/readability/Readability-readerable.js","statSize":4162},{"id":238,"label":"Readability.js","path":"./node_modules/@mozilla/readability/Readability.js","statSize":84033},{"id":396,"label":"index.js","path":"./node_modules/@mozilla/readability/index.js","statSize":226}],"parsedSize":0,"gzipSize":0},{"label":"remove-markdown","path":"./node_modules/remove-markdown","statSize":4061,"groups":[{"id":481,"label":"index.js","path":"./node_modules/remove-markdown/index.js","statSize":4061}],"parsedSize":0,"gzipSize":0}],"parsedSize":0,"gzipSize":0},{"label":"src","path":"./src","statSize":27608,"groups":[{"id":922,"label":"index.js + 1 modules (concatenated)","path":"./src/index.js + 1 modules (concatenated)","statSize":27608,"parsedSize":47998,"gzipSize":15433,"concatenated":true,"groups":[{"label":"src","path":"./src/index.js + 1 modules (concatenated)/src","statSize":2546,"groups":[{"id":null,"label":"index.js","path":"./src/index.js + 1 modules (concatenated)/src/index.js","statSize":2546,"parsedSize":4426,"gzipSize":1423,"inaccurateSizes":true}],"parsedSize":4426,"gzipSize":1423,"inaccurateSizes":true},{"label":"node_modules/turndown/lib","path":"./src/index.js + 1 modules (concatenated)/node_modules/turndown/lib","statSize":25062,"groups":[{"id":null,"label":"turndown.browser.es.js","path":"./src/index.js + 1 modules (concatenated)/node_modules/turndown/lib/turndown.browser.es.js","statSize":25062,"parsedSize":43571,"gzipSize":14009,"inaccurateSizes":true}],"parsedSize":43571,"gzipSize":14009,"inaccurateSizes":true}]}],"parsedSize":47998,"gzipSize":15433}],"isInitialByEntrypoint":{"main":true}}];
|
||||||
window.entrypoints = ["main"];
|
window.entrypoints = ["main"];
|
||||||
window.defaultSizes = "parsed";
|
window.defaultSizes = "parsed";
|
||||||
</script>
|
</script>
|
||||||
+515
-384
File diff suppressed because it is too large
Load Diff
+919
-1307
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -11,16 +11,16 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mozilla/readability": "^0.5.0",
|
"@mozilla/readability": "^0.5.0",
|
||||||
"dompurify": "^3.1.6"
|
"remove-markdown": "^0.5.5",
|
||||||
|
"turndown": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-env": "^7.24.7",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"babel-loader": "^9.1.3",
|
"babel-loader": "^9.2.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"compression-webpack-plugin": "^11.1.0",
|
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"webpack": "^5.92.1",
|
"webpack": "^5.96.1",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user