first bang implementation

This commit is contained in:
Fabian Freund
2024-06-10 03:08:23 +02:00
parent 9ae08409fe
commit 388eba93b7
47 changed files with 3809 additions and 34 deletions
+16
View File
@@ -0,0 +1,16 @@
targets:
$default:
builders:
drift_dev:
# These options change how drift generates code
options:
skip_verification_code: true
data_class_to_companions: false
named_parameters: true
sql:
dialect: sqlite
options:
version: "3.45"
modules:
- json1
- fts5
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+26 -2
View File
@@ -1,5 +1,7 @@
import 'package:bang_navigator/core/routing/dialog_page.dart';
import 'package:bang_navigator/features/about/presentation/screens/about.dart';
import 'package:bang_navigator/features/bangs/presentation/screens/bang.dart';
import 'package:bang_navigator/features/bangs/presentation/screens/search.dart';
import 'package:bang_navigator/features/chat_archive/presentation/screens/detail.dart';
import 'package:bang_navigator/features/chat_archive/presentation/screens/list.dart';
import 'package:bang_navigator/features/search_browser/presentation/screens/browser.dart';
@@ -17,11 +19,19 @@ part 'routes.g.dart';
name: 'AboutRoute',
path: 'about',
),
TypedGoRoute<BangRoute>(
name: 'BangRoute',
path: 'bangs',
routes: [
TypedGoRoute<BangSearchRoute>(
name: 'BangSearchRoute',
path: 'search',
),
],
),
],
)
class KagiRoute extends GoRouteData {
KagiRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const KagiScreen();
@@ -35,6 +45,20 @@ class AboutRoute extends GoRouteData {
}
}
class BangRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const BangScreen();
}
}
class BangSearchRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const BangSearchScreen();
}
}
@TypedGoRoute<SettingsRoute>(
name: 'SettingsRoute',
path: '/settings',
+46
View File
@@ -22,6 +22,18 @@ RouteBase get $kagiRoute => GoRouteData.$route(
name: 'AboutRoute',
factory: $AboutRouteExtension._fromState,
),
GoRouteData.$route(
path: 'bangs',
name: 'BangRoute',
factory: $BangRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'search',
name: 'BangSearchRoute',
factory: $BangSearchRouteExtension._fromState,
),
],
),
],
);
@@ -59,6 +71,40 @@ extension $AboutRouteExtension on AboutRoute {
void replace(BuildContext context) => context.replace(location);
}
extension $BangRouteExtension on BangRoute {
static BangRoute _fromState(GoRouterState state) => BangRoute();
String get location => GoRouteData.$location(
'/bangs',
);
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 $BangSearchRouteExtension on BangSearchRoute {
static BangSearchRoute _fromState(GoRouterState state) => BangSearchRoute();
String get location => GoRouteData.$location(
'/bangs/search',
);
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);
}
RouteBase get $settingsRoute => GoRouteData.$route(
path: '/settings',
name: 'SettingsRoute',
@@ -0,0 +1,98 @@
import 'package:bang_navigator/features/bangs/data/database/database.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:drift/drift.dart';
part 'bang.g.dart';
@DriftAccessor()
class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
BangDao(super.db);
Selectable<Bang> getBangs({Iterable<BangGroup?>? groups}) {
final selectable = select(db.bang);
if (groups != null) {
selectable.where((t) => t.group.isInValues(groups));
}
return selectable;
}
SingleOrNullSelectable<BangData> getBangData(String trigger) {
return select(db.bangDataView)..where((t) => t.trigger.equals(trigger));
}
Selectable<BangData> getBangDataList({Iterable<BangGroup?>? groups}) {
final selectable = select(db.bangDataView);
if (groups != null) {
selectable.where((t) => t.group.isInValues(groups));
}
selectable.orderBy([(t) => OrderingTerm.asc(t.websiteName)]);
return selectable;
}
Selectable<BangData> getFrequentBangDataList({Iterable<BangGroup?>? groups}) {
final selectable = select(db.bangDataView)
..where((t) => t.frequency.isBiggerThanValue(0));
if (groups != null) {
selectable.where((t) => t.group.isInValues(groups));
}
selectable.orderBy([
(t) => OrderingTerm.desc(t.frequency),
(t) => OrderingTerm.desc(t.lastUsed),
]);
return selectable;
}
Future<int> increaseFrequency(String trigger) {
return db.bangFrequency.insertOne(
BangFrequencyCompanion.insert(
trigger: trigger,
frequency: 1,
lastUsed: DateTime.now().toUtc(),
),
onConflict: DoUpdate(
(old) => BangFrequencyCompanion.custom(
frequency: old.frequency + const Constant(1),
lastUsed: Variable(DateTime.now().toUtc()),
),
),
);
}
Selectable<BangData> queryBangs(String searchString) {
return db.bangQuery(query: db.buildQuery(searchString));
}
Future<void> insertBangs(Iterable<Bang> bangs) {
return db.bang.insertAll(bangs);
}
Future<int> upsertIcon(String trigger, Uint8List iconData) {
return db.bangIcon.insertOne(
BangIconCompanion.insert(
trigger: trigger,
iconData: iconData,
fetchDate: DateTime.now().toUtc(),
),
mode: InsertMode.insertOrReplace,
);
}
Future<void> replaceBangs(Iterable<Bang> bangs) {
return batch(
(batch) {
batch.replaceAll(db.bang, bangs);
},
);
}
Future<int> deleteBangs(Iterable<String> triggers) {
final statement = delete(db.bang)..where((t) => t.trigger.isIn(triggers));
return statement.go();
}
}
@@ -0,0 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang.dart';
// ignore_for_file: type=lint
mixin _$BangDaoMixin on DatabaseAccessor<BangDatabase> {}
@@ -0,0 +1,28 @@
import 'package:bang_navigator/features/bangs/data/database/daos/bang.dart';
import 'package:bang_navigator/features/bangs/data/database/drift/converters/bang_format.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/query/domain/tokenizer.dart';
import 'package:drift/drift.dart';
part 'database.g.dart';
@DriftDatabase(include: {'database.drift'}, daos: [BangDao])
class BangDatabase extends _$BangDatabase with PrefixQueryBuilderMixin {
@override
final int schemaVersion = 1;
@override
final int ftsTokenLimit = 6;
@override
final int ftsMinTokenLength = 2;
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON;');
},
);
BangDatabase(super.e);
}
@@ -0,0 +1,88 @@
import 'package:bang_navigator/features/bangs/data/database/drift/converters/bang_format.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
CREATE TABLE bang (
"trigger" TEXT PRIMARY KEY NOT NULL,
"group" ENUM(BangGroup),
website_name TEXT NOT NULL,
domain TEXT NOT NULL,
url_template TEXT NOT NULL,
category TEXT,
sub_category TEXT,
format TEXT MAPPED BY `const BangFormatConverter()`
) WITH Bang;
CREATE TABLE bang_frequency (
"trigger" TEXT PRIMARY KEY NOT NULL REFERENCES bang ("trigger") ON DELETE CASCADE,
frequency INTEGER NOT NULL,
last_used DATETIME NOT NULL
);
CREATE TABLE bang_icon (
"trigger" TEXT PRIMARY KEY NOT NULL REFERENCES bang ("trigger") ON DELETE CASCADE,
icon_data BLOB NOT NULL,
fetch_date DATETIME NOT NULL
);
CREATE VIRTUAL TABLE bang_fts
USING fts5(
trigger,
website_name,
content=bang,
prefix='2 3'
);
CREATE VIEW bang_data_view WITH BangData AS
SELECT
b.*,
bf.frequency,
bf.last_used,
bi.icon_data
FROM
bang b
LEFT JOIN
bang_frequency bf ON b."trigger" = bf."trigger"
LEFT JOIN
bang_icon bi ON b."trigger" = bi."trigger";
-- Triggers to keep the FTS index up to date.
CREATE TRIGGER bang_after_insert AFTER INSERT ON bang BEGIN
INSERT INTO
bang_fts(rowid, "trigger", website_name)
VALUES (new.rowid, new."trigger", new.website_name);
END;
CREATE TRIGGER bang_after_delete AFTER DELETE ON bang BEGIN
INSERT INTO
bang_fts(bang_fts, rowid, "trigger", website_name)
VALUES('delete', old.rowid, old."trigger", old.website_name);
END;
CREATE TRIGGER bang_after_update AFTER UPDATE ON bang BEGIN
INSERT INTO
bang_fts(bang_fts, rowid, "trigger", website_name)
VALUES('delete', old.rowid, old."trigger", old.website_name);
INSERT INTO
bang_fts(rowid, "trigger", website_name)
VALUES (new.rowid, new."trigger", new.website_name);
END;
optimizeFtsIndex:
INSERT INTO bang_fts(bang_fts) VALUES ('optimize');
bangQuery WITH BangData:
SELECT
b.*,
bf.frequency,
bf.last_used,
bi.icon_data
FROM
bang_fts(:query) fts
INNER JOIN
bang b ON b.rowid = fts.rowid
LEFT JOIN
bang_frequency bf ON b."trigger" = bf."trigger"
LEFT JOIN
bang_icon bi ON b."trigger" = bi."trigger"
ORDER BY
RANK,
bf.frequency NULLS LAST;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
import 'dart:convert';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:drift/drift.dart';
class BangFormatConverter extends TypeConverter<Set<BangFormat>?, String?> {
const BangFormatConverter();
@override
Set<BangFormat>? fromSql(String? fromDb) {
if (fromDb == null) {
return null;
}
return Bang.decodeFormat(jsonDecode(fromDb) as List);
}
@override
String? toSql(Set<BangFormat>? value) {
if (value == null) {
return null;
}
return jsonEncode(Bang.encodeFormat(value));
}
}
@@ -0,0 +1,145 @@
import 'package:bang_navigator/features/bangs/data/database/database.dart'
show BangCompanion;
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:drift/drift.dart' show Expression, Insertable, Value;
import 'package:fast_equatable/fast_equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'bang.g.dart';
enum BangGroup {
assistant,
kagi,
}
enum BangFormat {
///When the bang is invoked with no query, opens the base path of the URL (/)
///instead of any path given in the template (g., /search)
@JsonValue('open_base_path')
openBasePath,
///URL encode the search terms. Some sites do not work with this, so it can
///be disabled by omitting this.
@JsonValue('url_encode_placeholder')
urlEncodePlaceholder,
///URL encodes spaces as +, instead of %20. Some sites only work correctly
///with one or the other.
@JsonValue('url_encode_space_to_plus')
urlEncodeSpaceToPlus,
}
@JsonSerializable()
@CopyWith()
class Bang with FastEquatable implements Insertable<Bang> {
static const _templateQueryPlaceholder = '{{{s}}}';
@JsonKey(includeFromJson: false, includeToJson: false)
final BangGroup? group;
///The name of the website associated with the bang.
@JsonKey(name: 's')
final String websiteName;
///The domain name of the websit
@JsonKey(name: 'd')
final String domain;
///The specific trigger word or phrase used to invoke the bang.
@JsonKey(name: 't')
final String trigger;
///The URL template to use when the bang is invoked, where `{{{s}}}` is replaced by the user's query.
@JsonKey(name: 'u')
final String urlTemplate;
///The category of the website, if applicable
@JsonKey(name: 'c')
final String? category;
///The subcategory of the website, if applicable
@JsonKey(name: 'sc')
final String? subCategory;
///The format flags indicating how the query should be processed.
@JsonKey(name: 'fmt')
final Set<BangFormat>? format;
String formatQuery(String input) {
return (format == null ||
format!.contains(BangFormat.urlEncodePlaceholder) == true)
? (format == null ||
format?.contains(BangFormat.urlEncodeSpaceToPlus) == true)
? Uri.encodeQueryComponent(input)
: Uri.encodeComponent(input)
: input;
}
Uri getUrl(String? query) {
final url = (query != null)
? urlTemplate.replaceAll(_templateQueryPlaceholder, formatQuery(query))
: urlTemplate;
var template = Uri.parse(url);
if (!template.hasScheme || template.origin.isEmpty) {
template = Uri.https(domain).replace(
path: template.path,
query: template.query,
);
}
return template;
}
static Set<BangFormat> decodeFormat(Iterable input) {
return input.map((e) => $enumDecode(_$BangFormatEnumMap, e)).toSet();
}
static List<String> encodeFormat(Iterable<BangFormat> format) {
return format.map((e) => _$BangFormatEnumMap[e]!).toList();
}
Bang({
required this.websiteName,
required this.domain,
required this.trigger,
required this.urlTemplate,
this.group,
this.category,
this.subCategory,
this.format,
});
factory Bang.fromJson(Map<String, dynamic> json) => _$BangFromJson(json);
Map<String, dynamic> toJson() => _$BangToJson(this);
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
group,
websiteName,
domain,
trigger,
urlTemplate,
category,
subCategory,
format,
];
@override
Map<String, Expression<Object>> toColumns(bool nullToAbsent) {
return BangCompanion(
trigger: Value(trigger),
websiteName: Value(websiteName),
domain: Value(domain),
urlTemplate: Value(urlTemplate),
group: Value.absentIfNull(group),
category: Value.absentIfNull(category),
subCategory: Value.absentIfNull(subCategory),
format: Value.absentIfNull(format),
).toColumns(nullToAbsent);
}
}
@@ -0,0 +1,167 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$BangCWProxy {
Bang websiteName(String websiteName);
Bang domain(String domain);
Bang trigger(String trigger);
Bang urlTemplate(String urlTemplate);
Bang group(BangGroup? group);
Bang category(String? category);
Bang subCategory(String? subCategory);
Bang format(Set<BangFormat>? format);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Bang(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// Bang(...).copyWith(id: 12, name: "My name")
/// ````
Bang call({
String? websiteName,
String? domain,
String? trigger,
String? urlTemplate,
BangGroup? group,
String? category,
String? subCategory,
Set<BangFormat>? format,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfBang.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfBang.copyWith.fieldName(...)`
class _$BangCWProxyImpl implements _$BangCWProxy {
const _$BangCWProxyImpl(this._value);
final Bang _value;
@override
Bang websiteName(String websiteName) => this(websiteName: websiteName);
@override
Bang domain(String domain) => this(domain: domain);
@override
Bang trigger(String trigger) => this(trigger: trigger);
@override
Bang urlTemplate(String urlTemplate) => this(urlTemplate: urlTemplate);
@override
Bang group(BangGroup? group) => this(group: group);
@override
Bang category(String? category) => this(category: category);
@override
Bang subCategory(String? subCategory) => this(subCategory: subCategory);
@override
Bang format(Set<BangFormat>? format) => this(format: format);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Bang(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// Bang(...).copyWith(id: 12, name: "My name")
/// ````
Bang call({
Object? websiteName = const $CopyWithPlaceholder(),
Object? domain = const $CopyWithPlaceholder(),
Object? trigger = const $CopyWithPlaceholder(),
Object? urlTemplate = const $CopyWithPlaceholder(),
Object? group = const $CopyWithPlaceholder(),
Object? category = const $CopyWithPlaceholder(),
Object? subCategory = const $CopyWithPlaceholder(),
Object? format = const $CopyWithPlaceholder(),
}) {
return Bang(
websiteName:
websiteName == const $CopyWithPlaceholder() || websiteName == null
? _value.websiteName
// ignore: cast_nullable_to_non_nullable
: websiteName as String,
domain: domain == const $CopyWithPlaceholder() || domain == null
? _value.domain
// ignore: cast_nullable_to_non_nullable
: domain as String,
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
? _value.trigger
// ignore: cast_nullable_to_non_nullable
: trigger as String,
urlTemplate:
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
? _value.urlTemplate
// ignore: cast_nullable_to_non_nullable
: urlTemplate as String,
group: group == const $CopyWithPlaceholder()
? _value.group
// ignore: cast_nullable_to_non_nullable
: group as BangGroup?,
category: category == const $CopyWithPlaceholder()
? _value.category
// ignore: cast_nullable_to_non_nullable
: category as String?,
subCategory: subCategory == const $CopyWithPlaceholder()
? _value.subCategory
// ignore: cast_nullable_to_non_nullable
: subCategory as String?,
format: format == const $CopyWithPlaceholder()
? _value.format
// ignore: cast_nullable_to_non_nullable
: format as Set<BangFormat>?,
);
}
}
extension $BangCopyWith on Bang {
/// Returns a callable class that can be used as follows: `instanceOfBang.copyWith(...)` or like so:`instanceOfBang.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$BangCWProxy get copyWith => _$BangCWProxyImpl(this);
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Bang _$BangFromJson(Map<String, dynamic> json) => Bang(
websiteName: json['s'] as String,
domain: json['d'] as String,
trigger: json['t'] as String,
urlTemplate: json['u'] as String,
category: json['c'] as String?,
subCategory: json['sc'] as String?,
format: (json['fmt'] as List<dynamic>?)
?.map((e) => $enumDecode(_$BangFormatEnumMap, e))
.toSet(),
);
Map<String, dynamic> _$BangToJson(Bang instance) => <String, dynamic>{
's': instance.websiteName,
'd': instance.domain,
't': instance.trigger,
'u': instance.urlTemplate,
'c': instance.category,
'sc': instance.subCategory,
'fmt': instance.format?.map((e) => _$BangFormatEnumMap[e]!).toList(),
};
const _$BangFormatEnumMap = {
BangFormat.openBasePath: 'open_base_path',
BangFormat.urlEncodePlaceholder: 'url_encode_placeholder',
BangFormat.urlEncodeSpaceToPlus: 'url_encode_space_to_plus',
};
@@ -0,0 +1,37 @@
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:drift/drift.dart';
part 'bang_data.g.dart';
@CopyWith()
class BangData extends Bang {
final int frequency;
final DateTime? lastUsed;
final Uint8List? iconData;
BangData({
required super.websiteName,
required super.domain,
required super.trigger,
required super.urlTemplate,
super.category,
super.subCategory,
super.format,
int? frequency,
this.lastUsed,
this.iconData,
}) : frequency = frequency ?? 0;
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
...super.hashParameters,
frequency,
lastUsed,
iconData,
];
}
@@ -0,0 +1,157 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang_data.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$BangDataCWProxy {
BangData websiteName(String websiteName);
BangData domain(String domain);
BangData trigger(String trigger);
BangData urlTemplate(String urlTemplate);
BangData category(String? category);
BangData subCategory(String? subCategory);
BangData format(Set<BangFormat>? format);
BangData frequency(int? frequency);
BangData lastUsed(DateTime? lastUsed);
BangData iconData(Uint8List? iconData);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `BangData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// BangData(...).copyWith(id: 12, name: "My name")
/// ````
BangData call({
String? websiteName,
String? domain,
String? trigger,
String? urlTemplate,
String? category,
String? subCategory,
Set<BangFormat>? format,
int? frequency,
DateTime? lastUsed,
Uint8List? iconData,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfBangData.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfBangData.copyWith.fieldName(...)`
class _$BangDataCWProxyImpl implements _$BangDataCWProxy {
const _$BangDataCWProxyImpl(this._value);
final BangData _value;
@override
BangData websiteName(String websiteName) => this(websiteName: websiteName);
@override
BangData domain(String domain) => this(domain: domain);
@override
BangData trigger(String trigger) => this(trigger: trigger);
@override
BangData urlTemplate(String urlTemplate) => this(urlTemplate: urlTemplate);
@override
BangData category(String? category) => this(category: category);
@override
BangData subCategory(String? subCategory) => this(subCategory: subCategory);
@override
BangData format(Set<BangFormat>? format) => this(format: format);
@override
BangData frequency(int? frequency) => this(frequency: frequency);
@override
BangData lastUsed(DateTime? lastUsed) => this(lastUsed: lastUsed);
@override
BangData iconData(Uint8List? iconData) => this(iconData: iconData);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `BangData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// BangData(...).copyWith(id: 12, name: "My name")
/// ````
BangData call({
Object? websiteName = const $CopyWithPlaceholder(),
Object? domain = const $CopyWithPlaceholder(),
Object? trigger = const $CopyWithPlaceholder(),
Object? urlTemplate = const $CopyWithPlaceholder(),
Object? category = const $CopyWithPlaceholder(),
Object? subCategory = const $CopyWithPlaceholder(),
Object? format = const $CopyWithPlaceholder(),
Object? frequency = const $CopyWithPlaceholder(),
Object? lastUsed = const $CopyWithPlaceholder(),
Object? iconData = const $CopyWithPlaceholder(),
}) {
return BangData(
websiteName:
websiteName == const $CopyWithPlaceholder() || websiteName == null
? _value.websiteName
// ignore: cast_nullable_to_non_nullable
: websiteName as String,
domain: domain == const $CopyWithPlaceholder() || domain == null
? _value.domain
// ignore: cast_nullable_to_non_nullable
: domain as String,
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
? _value.trigger
// ignore: cast_nullable_to_non_nullable
: trigger as String,
urlTemplate:
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
? _value.urlTemplate
// ignore: cast_nullable_to_non_nullable
: urlTemplate as String,
category: category == const $CopyWithPlaceholder()
? _value.category
// ignore: cast_nullable_to_non_nullable
: category as String?,
subCategory: subCategory == const $CopyWithPlaceholder()
? _value.subCategory
// ignore: cast_nullable_to_non_nullable
: subCategory as String?,
format: format == const $CopyWithPlaceholder()
? _value.format
// ignore: cast_nullable_to_non_nullable
: format as Set<BangFormat>?,
frequency: frequency == const $CopyWithPlaceholder()
? _value.frequency
// ignore: cast_nullable_to_non_nullable
: frequency as int?,
lastUsed: lastUsed == const $CopyWithPlaceholder()
? _value.lastUsed
// ignore: cast_nullable_to_non_nullable
: lastUsed as DateTime?,
iconData: iconData == const $CopyWithPlaceholder()
? _value.iconData
// ignore: cast_nullable_to_non_nullable
: iconData as Uint8List?,
);
}
}
extension $BangDataCopyWith on BangData {
/// Returns a callable class that can be used as follows: `instanceOfBangData.copyWith(...)` or like so:`instanceOfBangData.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$BangDataCWProxy get copyWith => _$BangDataCWProxyImpl(this);
}
@@ -0,0 +1,37 @@
import 'package:bang_navigator/features/bangs/data/database/database.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:universal_io/io.dart';
part 'providers.g.dart';
@Riverpod(keepAlive: true)
BangDatabase bangDatabase(BangDatabaseRef ref) {
return BangDatabase(
LazyDatabase(() async {
// put the database file, called db.sqlite here, into the documents folder
// for your app.
final dbFolder = await path_provider.getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'bang.db'));
// Also work around limitations on old Android versions
if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}
// Make sqlite3 pick a more suitable location for temporary files - the
// one from the system may be inaccessible due to sandboxing.
final cachebase = (await path_provider.getTemporaryDirectory()).path;
// We can't access /tmp on Android, which sqlite3 would try by default.
// Explicitly tell it about the correct temporary directory.
sqlite3.tempDirectory = cachebase;
return NativeDatabase.createInBackground(file);
}),
);
}
@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bangDatabaseHash() => r'bbca2edf32faa378dcb6270b12019758d5156662';
/// See also [bangDatabase].
@ProviderFor(bangDatabase)
final bangDatabaseProvider = Provider<BangDatabase>.internal(
bangDatabase,
name: r'bangDatabaseProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bangDatabaseHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef BangDatabaseRef = ProviderRef<BangDatabase>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,43 @@
import 'dart:convert';
import 'package:bang_navigator/core/http_error_handler.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:exceptions/exceptions.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bang_source.g.dart';
@Riverpod()
class BangSource extends _$BangSource {
late http.Client _client;
@override
void build() {
_client = http.Client();
}
Future<Result<List<Bang>>> getBangs(Uri url, BangGroup? group) async {
return Result.fromAsync(
() async {
final response = await _client.get(url);
return await compute(
(args) => jsonDecode(utf8.decode(args[0])) as List,
[response.bodyBytes],
).then(
(json) => json.map((e) {
var bang = Bang.fromJson(e as Map<String, dynamic>);
if (group != null) {
bang = bang.copyWith.group(group);
}
return bang;
}).toList(),
);
},
exceptionHandler: handleHttpError,
);
}
}
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang_source.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bangSourceHash() => r'6ca70e8be16a0ae104fa6b739e3c0f2c59eab565';
/// See also [BangSource].
@ProviderFor(BangSource)
final bangSourceProvider =
AutoDisposeNotifierProvider<BangSource, void>.internal(
BangSource.new,
name: r'bangSourceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bangSourceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BangSource = AutoDisposeNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,40 @@
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
@Riverpod()
Stream<BangData?> bang(BangRef ref, String? trigger) {
final repository = ref.watch(bangRepositoryProvider.notifier);
return repository.watchBang(trigger);
}
@Riverpod(keepAlive: true)
Stream<BangData?> kagiSearchBang(KagiSearchBangRef ref) {
final repository = ref.watch(bangRepositoryProvider.notifier);
return repository.watchBang('kagi');
}
@Riverpod()
Stream<List<BangData>> allBangs(AllBangsRef ref) {
final repository = ref.watch(bangRepositoryProvider.notifier);
return repository.watchBangs();
}
@Riverpod()
Stream<List<BangData>> frequentBangs(FrequentBangsRef ref) {
final repository = ref.watch(bangRepositoryProvider.notifier);
return repository.watchFrequentBangs();
}
@Riverpod()
Future<BangData> ensureIconAvailable(
EnsureIconAvailableRef ref,
BangData bang,
) {
final repository = ref.watch(bangRepositoryProvider.notifier);
return repository.ensureIconAvailable(bang).then(
(value) => value.value,
);
}
@@ -0,0 +1,330 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bangHash() => r'e3d44af52e27831b568f292d38b447a443acec95';
/// 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));
}
}
/// See also [bang].
@ProviderFor(bang)
const bangProvider = BangFamily();
/// See also [bang].
class BangFamily extends Family<AsyncValue<BangData?>> {
/// See also [bang].
const BangFamily();
/// See also [bang].
BangProvider call(
String? trigger,
) {
return BangProvider(
trigger,
);
}
@override
BangProvider getProviderOverride(
covariant BangProvider provider,
) {
return call(
provider.trigger,
);
}
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'bangProvider';
}
/// See also [bang].
class BangProvider extends AutoDisposeStreamProvider<BangData?> {
/// See also [bang].
BangProvider(
String? trigger,
) : this._internal(
(ref) => bang(
ref as BangRef,
trigger,
),
from: bangProvider,
name: r'bangProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bangHash,
dependencies: BangFamily._dependencies,
allTransitiveDependencies: BangFamily._allTransitiveDependencies,
trigger: trigger,
);
BangProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.trigger,
}) : super.internal();
final String? trigger;
@override
Override overrideWith(
Stream<BangData?> Function(BangRef provider) create,
) {
return ProviderOverride(
origin: this,
override: BangProvider._internal(
(ref) => create(ref as BangRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
trigger: trigger,
),
);
}
@override
AutoDisposeStreamProviderElement<BangData?> createElement() {
return _BangProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BangProvider && other.trigger == trigger;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, trigger.hashCode);
return _SystemHash.finish(hash);
}
}
mixin BangRef on AutoDisposeStreamProviderRef<BangData?> {
/// The parameter `trigger` of this provider.
String? get trigger;
}
class _BangProviderElement extends AutoDisposeStreamProviderElement<BangData?>
with BangRef {
_BangProviderElement(super.provider);
@override
String? get trigger => (origin as BangProvider).trigger;
}
String _$kagiSearchBangHash() => r'ec830f26cf1d23e1e75747e1523cb1868e77d40b';
/// See also [kagiSearchBang].
@ProviderFor(kagiSearchBang)
final kagiSearchBangProvider = StreamProvider<BangData?>.internal(
kagiSearchBang,
name: r'kagiSearchBangProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$kagiSearchBangHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef KagiSearchBangRef = StreamProviderRef<BangData?>;
String _$allBangsHash() => r'212201f81e21fd0c1b5fb192359fb6ff8269a2a6';
/// See also [allBangs].
@ProviderFor(allBangs)
final allBangsProvider = AutoDisposeStreamProvider<List<BangData>>.internal(
allBangs,
name: r'allBangsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$allBangsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef AllBangsRef = AutoDisposeStreamProviderRef<List<BangData>>;
String _$frequentBangsHash() => r'12867819c6e2bbc5f2b6342aacc286f62d3a1803';
/// See also [frequentBangs].
@ProviderFor(frequentBangs)
final frequentBangsProvider =
AutoDisposeStreamProvider<List<BangData>>.internal(
frequentBangs,
name: r'frequentBangsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$frequentBangsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FrequentBangsRef = AutoDisposeStreamProviderRef<List<BangData>>;
String _$ensureIconAvailableHash() =>
r'2e112c88da8cf9bf12bdc5e5d525d59fe97a9419';
/// See also [ensureIconAvailable].
@ProviderFor(ensureIconAvailable)
const ensureIconAvailableProvider = EnsureIconAvailableFamily();
/// See also [ensureIconAvailable].
class EnsureIconAvailableFamily extends Family<AsyncValue<BangData>> {
/// See also [ensureIconAvailable].
const EnsureIconAvailableFamily();
/// See also [ensureIconAvailable].
EnsureIconAvailableProvider call(
BangData bang,
) {
return EnsureIconAvailableProvider(
bang,
);
}
@override
EnsureIconAvailableProvider getProviderOverride(
covariant EnsureIconAvailableProvider provider,
) {
return call(
provider.bang,
);
}
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'ensureIconAvailableProvider';
}
/// See also [ensureIconAvailable].
class EnsureIconAvailableProvider extends AutoDisposeFutureProvider<BangData> {
/// See also [ensureIconAvailable].
EnsureIconAvailableProvider(
BangData bang,
) : this._internal(
(ref) => ensureIconAvailable(
ref as EnsureIconAvailableRef,
bang,
),
from: ensureIconAvailableProvider,
name: r'ensureIconAvailableProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$ensureIconAvailableHash,
dependencies: EnsureIconAvailableFamily._dependencies,
allTransitiveDependencies:
EnsureIconAvailableFamily._allTransitiveDependencies,
bang: bang,
);
EnsureIconAvailableProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.bang,
}) : super.internal();
final BangData bang;
@override
Override overrideWith(
FutureOr<BangData> Function(EnsureIconAvailableRef provider) create,
) {
return ProviderOverride(
origin: this,
override: EnsureIconAvailableProvider._internal(
(ref) => create(ref as EnsureIconAvailableRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
bang: bang,
),
);
}
@override
AutoDisposeFutureProviderElement<BangData> createElement() {
return _EnsureIconAvailableProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is EnsureIconAvailableProvider && other.bang == bang;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, bang.hashCode);
return _SystemHash.finish(hash);
}
}
mixin EnsureIconAvailableRef on AutoDisposeFutureProviderRef<BangData> {
/// The parameter `bang` of this provider.
BangData get bang;
}
class _EnsureIconAvailableProviderElement
extends AutoDisposeFutureProviderElement<BangData>
with EnsureIconAvailableRef {
_EnsureIconAvailableProviderElement(super.provider);
@override
BangData get bang => (origin as EnsureIconAvailableProvider).bang;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,154 @@
import 'package:bang_navigator/domain/services/generic_website.dart';
import 'package:bang_navigator/features/bangs/data/database/database.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/data/providers.dart';
import 'package:bang_navigator/features/bangs/data/services/bang_source.dart';
import 'package:bang_navigator/utils/image_helper.dart';
import 'package:drift/isolate.dart';
import 'package:exceptions/exceptions.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bang.g.dart';
Future<Result<void>> _syncBangGroupStatic(
BangSource source,
BangDatabase db,
Uri url,
BangGroup? group,
) async {
final result = await source.getBangs(url, group);
return result.flatMapAsync(
(value) async {
final sourcedBangMap =
Map.fromEntries(value.map((e) => MapEntry(e.trigger, e)));
final localBangMap =
await db.bangDao.getBangs(groups: [group]).get().then(
(bangs) =>
Map.fromEntries(bangs.map((e) => MapEntry(e.trigger, e))),
);
final sourcedBangTriggers = sourcedBangMap.keys.toSet();
final localBangTriggers = localBangMap.keys.toSet();
final removedBangs = localBangTriggers.difference(sourcedBangTriggers);
final addedBangs = sourcedBangTriggers.difference(localBangTriggers).map(
(e) => sourcedBangMap[e]!,
);
final changedBangs = sourcedBangTriggers
.intersection(localBangTriggers)
.where((e) => sourcedBangMap[e] != localBangMap[e])
.map(
(e) => sourcedBangMap[e]!,
);
await db.bangDao.deleteBangs(removedBangs);
await db.bangDao.insertBangs(addedBangs);
await db.bangDao.replaceBangs(changedBangs);
await db.optimizeFtsIndex();
},
);
}
@Riverpod(keepAlive: true)
class BangRepository extends _$BangRepository {
late BangDatabase _db;
@override
void build() {
_db = ref.watch(bangDatabaseProvider);
}
Stream<BangData?> watchBang(String? trigger) {
if (trigger != null) {
return _db.bangDao.getBangData(trigger).watchSingleOrNull();
} else {
return Stream.value(null);
}
}
Stream<List<BangData>> watchBangs({Iterable<BangGroup?>? groups}) {
return _db.bangDao.getBangDataList(groups: groups).watch();
}
Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup?>? groups}) {
return _db.bangDao.getFrequentBangDataList(groups: groups).watch();
}
Future<void> increaseFrequency(String trigger) {
return _db.bangDao.increaseFrequency(trigger);
}
Future<Result<BangData>> ensureIconAvailable(BangData bang) async {
if (bang.iconData != null) {
return Result.success(bang);
}
final result = await ref
.read(genericWebsiteServiceProvider.notifier)
.getFaviconBytes(Uri.parse(bang.getUrl('').origin));
return result.flatMapAsync(
(faviconBytes) async {
if (faviconBytes != null && await isImageValid(faviconBytes)) {
await _db.bangDao.upsertIcon(bang.trigger, faviconBytes);
return bang.copyWith.iconData(faviconBytes);
}
return bang;
},
);
}
Future<Result<void>> syncBangGroup(
Uri url,
BangGroup? group,
) =>
_syncBangGroupStatic(
ref.read(bangSourceProvider.notifier),
_db,
url,
group,
);
Future<void> syncAllBangGroups() async {
await _db.computeWithDatabase(
connect: BangDatabase.new,
computation: (db) async {
final ref = ProviderContainer();
final source = ref.read(bangSourceProvider.notifier);
await _syncBangGroupStatic(
source,
db,
Uri.parse(
'https://raw.githubusercontent.com/kagisearch/bangs/main/data/bangs.json',
),
null,
);
await _syncBangGroupStatic(
source,
db,
Uri.parse(
'https://raw.githubusercontent.com/kagisearch/bangs/main/data/kagi_bangs.json',
),
BangGroup.kagi,
);
await _syncBangGroupStatic(
source,
db,
Uri.parse(
'https://raw.githubusercontent.com/kagisearch/bangs/main/data/assistant_bangs.json',
),
BangGroup.assistant,
);
},
);
}
}
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bangRepositoryHash() => r'd405550e2ad11c57f3644f18390348efb71bdf0f';
/// See also [BangRepository].
@ProviderFor(BangRepository)
final bangRepositoryProvider = NotifierProvider<BangRepository, void>.internal(
BangRepository.new,
name: r'bangRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$bangRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BangRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,32 @@
import 'dart:async';
import 'package:bang_navigator/features/bangs/data/database/database.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bang_search.g.dart';
@Riverpod()
class BangSearch extends _$BangSearch {
late StreamController<List<BangData>> _streamController;
late BangDatabase _db;
Future<void> search(String input) async {
if (input.isNotEmpty) {
await _db.bangDao.queryBangs(input).get().then(_streamController.add);
}
}
@override
Stream<List<BangData>> build() {
_db = ref.watch(bangDatabaseProvider);
_streamController = StreamController();
ref.onDispose(() async {
await _streamController.close();
});
return _streamController.stream;
}
}
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bang_search.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$bangSearchHash() => r'abd71f10288ac1f559e66fc11035f9f2b027c353';
/// See also [BangSearch].
@ProviderFor(BangSearch)
final bangSearchProvider =
AutoDisposeStreamNotifierProvider<BangSearch, List<BangData>>.internal(
BangSearch.new,
name: r'bangSearchProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bangSearchHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$BangSearch = AutoDisposeStreamNotifier<List<BangData>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,61 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_details.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/modes.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangScreen extends HookConsumerWidget {
const BangScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bangsAsync = ref.watch(allBangsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Bangs'),
actions: [
IconButton(
onPressed: () async {
await context.push(BangSearchRoute().location);
},
icon: const Icon(Icons.search),
),
],
),
body: bangsAsync.when(
data: (bangs) {
return ListView.builder(
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
return BangDetails(
bang,
onTap: () {
ref
.read(selectedBangTriggerProvider.notifier)
.setTrigger(bang.trigger);
if (ref.read(bottomSheetProvider) is! CreateTab) {
ref.read(bottomSheetProvider.notifier).show(
CreateTab(
preferredTool: KagiTool.search,
),
);
}
context.go(KagiRoute().location);
},
);
},
);
},
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
),
);
}
}
@@ -0,0 +1,82 @@
import 'dart:async';
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang_search.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_details.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/modes.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/presentation/hooks/listenable_callback.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangSearchScreen extends HookConsumerWidget {
const BangSearchScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final resultsAsync = ref.watch(bangSearchProvider);
final textEditingController = useTextEditingController();
useListenableCallback(textEditingController, () async {
unawaited(
ref
.read(bangSearchProvider.notifier)
.search(textEditingController.text),
);
});
return Scaffold(
appBar: AppBar(
title: TextField(
controller: textEditingController,
decoration: const InputDecoration.collapsed(hintText: 'Search'),
),
actions: [
IconButton(
onPressed: () {
if (textEditingController.text.isEmpty) {
context.pop();
} else {
textEditingController.clear();
}
},
icon: const Icon(Icons.clear),
),
],
),
body: resultsAsync.when(
skipLoadingOnReload: true,
data: (bangs) => ListView.builder(
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
return BangDetails(
bang,
onTap: () {
ref
.read(selectedBangTriggerProvider.notifier)
.setTrigger(bang.trigger);
if (ref.read(bottomSheetProvider) is! CreateTab) {
ref.read(bottomSheetProvider.notifier).show(
CreateTab(
preferredTool: KagiTool.search,
),
);
}
context.go(KagiRoute().location);
},
);
},
),
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
),
);
}
}
@@ -0,0 +1,106 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/features/web_view/presentation/controllers/switch_new_tab.dart';
import 'package:bang_navigator/utils/ui_helper.dart' as ui_helper;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangDetails extends HookConsumerWidget {
final BangData bangData;
final void Function()? onTap;
const BangDetails(this.bangData, {this.onTap, super.key});
String? _categoryString(BangData bang) {
if (bang.category == null) {
return null;
} else if (bang.subCategory == null) {
return bang.category;
} else {
return '${bang.category} / ${bang.subCategory}';
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
BangIcon(bangData),
const SizedBox(width: 12.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
bangData.websiteName.trim(),
style: theme.textTheme.titleMedium,
),
if (bangData.category != null)
Text(
_categoryString(bangData)!,
style: theme.textTheme.titleSmall,
),
],
),
),
],
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FilledButton.tonalIcon(
style:
const ButtonStyle(visualDensity: VisualDensity.compact),
onPressed: () async {
final url = Uri.parse(bangData.getUrl('').origin);
final launchExternal = ref
.read(settingsRepositoryProvider)
.valueOrNull
?.launchUrlExternal ??
false;
if (launchExternal) {
await ui_helper.launchUrlFeedback(context, url);
} else {
await ref
.read(switchNewTabControllerProvider.notifier)
.add(url);
if (context.mounted) {
context.go(KagiRoute().location);
}
}
},
label: Text(bangData.domain),
icon: const Icon(Icons.open_in_new),
),
Text(
'!${bangData.trigger}',
style: theme.textTheme.titleSmall,
),
],
),
],
),
),
),
);
}
}
@@ -0,0 +1,43 @@
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
class BangIcon extends HookConsumerWidget {
final double iconSize;
final BangData bangData;
const BangIcon(this.bangData, {this.iconSize = 34.0, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bangAsync = ref.watch(ensureIconAvailableProvider(bangData));
final bang = bangAsync.valueOrNull ?? bangData;
return Skeletonizer(
enabled: bangAsync.isLoading,
child: SizedBox.square(
dimension: iconSize,
child: (bang.iconData != null)
? Image.memory(
bang.iconData!,
height: iconSize,
width: iconSize,
fit: BoxFit.fill,
errorBuilder: (context, error, stackTrace) {
return Icon(
MdiIcons.web,
size: iconSize,
);
},
)
: Icon(
MdiIcons.web,
size: iconSize,
),
),
);
}
}
@@ -64,7 +64,9 @@ class ChatArchiveRepository extends _$ChatArchiveRepository {
@Riverpod()
Future<String> readArchivedChat(
ReadArchivedChatRef ref, String fileName) async {
ReadArchivedChatRef ref,
String fileName,
) async {
final result =
await ref.read(chatArchiveRepositoryProvider.notifier).readChat(fileName);
@@ -0,0 +1,6 @@
abstract interface class IQueryBuilder {
int get ftsTokenLimit;
int get ftsMinTokenLength;
String buildQuery(String input);
}
@@ -0,0 +1,70 @@
const barewordConcat = ' ';
sealed class Bareword {
final String word;
@override
int get hashCode => word.hashCode;
Bareword(String word) : word = word.replaceAll('"', '""');
@override
bool operator ==(Object other) {
if (other is Bareword) {
return word == other.word;
}
return false;
}
JoinedBareword join(Bareword other) {
return JoinedBareword(other.word, this);
}
}
final class SimpleBareword extends Bareword {
SimpleBareword(super.word);
@override
String toString() => word;
}
final class EnclosedBareword extends Bareword {
EnclosedBareword(super.word);
@override
String toString() {
return '"$word"';
}
}
final class JoinedBareword extends Bareword {
final Bareword parent;
JoinedBareword(super.bareword, this.parent);
bool hasDependency(Bareword other) {
if (parent is JoinedBareword) {
return (parent as JoinedBareword).hasDependency(other);
} else {
return parent == other;
}
}
Iterable<Bareword> get dependencies {
final depencies = <Bareword>[parent];
while (depencies.last is JoinedBareword) {
depencies.add((depencies.last as JoinedBareword).parent);
}
return depencies.reversed;
}
@override
String toString() {
final deps = dependencies
.map((bareword) => bareword.toString())
.join(barewordConcat);
return '"$deps$barewordConcat$word"';
}
}
@@ -0,0 +1,241 @@
import 'dart:math' as math;
import 'package:bang_navigator/features/query/domain/entities/abstract/i_query_builder.dart';
import 'package:bang_navigator/features/query/domain/entities/bareword.dart';
import 'package:collection/collection.dart';
typedef _Phrase = List<Bareword>;
sealed class QueryBuilder {
///Matching quoted strings like "xda bda" (group 1), as well as strings delimetered by
///a whitespace (group 2)
static final _tokenizePattern = RegExp('"([^"]+)"|([^ ]+)');
///Reserved FTS5 baewords
static const _reservedBarewords = {'AND', 'OR', 'NOT'};
///Checks if string is an bareword accoring to:
///https://www.sqlite.org/fts5.html#fts5_strings
///
///Elsewise ths string needs to get quoted
static final _barewordPattern =
RegExp(r'^([^\x00-\x7F]|[\w]|[\d]|[_]|[\x1A])+$');
static bool _isReservedBareword(Bareword input) =>
_reservedBarewords.contains(input.word);
late final _Phrase _tokens;
QueryBuilder.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 if (_barewordPattern.hasMatch(match.group(2)!)) {
return SimpleBareword(match.group(2)!);
} else {
return EnclosedBareword(match.group(2)!);
}
})
.where((token) => token.word.isNotEmpty)
.whereNot(_isReservedBareword)
.toList();
//Merge short tokens
_mergeShortBarewords(barewords, minTokenLength);
_tokens = barewords.take(tokenLimit).toList();
}
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);
}
}
}
}
//Get weighted groups of tokens
//Weight depends on order
static List<_Phrase> _getWeighted(List<Bareword> tokens) {
if (tokens.isEmpty) {
return [];
}
final phrases = List.generate(
tokens.length - 1,
(index) => tokens.sublist(0, tokens.length - index),
);
phrases.addAll(tokens.map((token) => [token]));
return phrases;
}
//Get all combinations of input tokens and treat them as equally
//Match of bigger group will have greater score
static List<_Phrase> _getCombinations(List<Bareword> tokens) {
if (tokens.isEmpty) {
return [];
}
final combinations = <_Phrase>[];
final slent = math.pow(2, tokens.length);
for (var i = 0; i < slent; i++) {
final temp = <Bareword>[];
for (var j = 0; j < tokens.length; j++) {
if ((i & math.pow(2, j).toInt()) > 0) {
temp.add(tokens[j]);
}
}
if (temp.isNotEmpty) {
combinations.add(temp);
}
}
return combinations;
}
//If we have multiple unenclosed token, combine them into quoted to
//get a better full range match
//TODO: Before migration this was only used for non-prefix tokens, check for side effects
static List<_Phrase> _combineUnenclosed(_Phrase phrase) {
return [
phrase,
if (phrase.length > 2 &&
phrase.every((bareword) => bareword is SimpleBareword))
[EnclosedBareword(phrase.join(barewordConcat))],
];
}
//Decinding query strategy
static List<_Phrase> _generatePhrases(List<Bareword> tokens) {
if (tokens.length <= 4) {
return _getCombinations(tokens);
} else if (tokens.length <= 8) {
return _getWeighted(tokens);
} else {
return [tokens];
}
}
static _Phrase _removeDependendBarewords(List<Bareword> phrase) {
final joinedPhrase = List.of(phrase);
for (var i = 0; i < joinedPhrase.length; i++) {
final bareword = joinedPhrase[i];
//Remove dependend phrases
if (joinedPhrase.whereType<JoinedBareword>().any(
(joined) => joined.dependencies.contains(bareword),
)) {
joinedPhrase.removeAt(i);
i--;
}
}
return joinedPhrase;
}
String _buildQuery(
List<Bareword> tokens,
String Function(List<Bareword> phrase) barewordConcat,
String phraseConcat,
) {
final phrases = _generatePhrases(tokens)
.map(_removeDependendBarewords)
.expand(_combineUnenclosed);
return phrases
.where((phrase) => phrase.isNotEmpty)
.sortedBy<num>((x) => x.length)
.reversed
.map(barewordConcat)
.toSet()
.join(phraseConcat);
}
String build();
}
final class PrefixQueryBuilder extends QueryBuilder {
PrefixQueryBuilder.tokenize({
required super.input,
required super.minTokenLength,
required super.tokenLimit,
}) : super.tokenize();
@override
String build() {
return _buildQuery(
_tokens,
(phrase) {
if (phrase.length == 1) {
return '${phrase.first}*';
}
return 'NEAR(${phrase.map((bareword) => '$bareword*').join(' ')})';
},
' OR ',
);
}
}
final class TrigramQueryBuilder extends QueryBuilder {
TrigramQueryBuilder.tokenize({
required super.input,
required super.minTokenLength,
required super.tokenLimit,
}) : super.tokenize();
@override
String build() {
return _buildQuery(
_tokens,
(phrase) => phrase.join(' '),
' OR ',
);
}
}
mixin PrefixQueryBuilderMixin implements IQueryBuilder {
@override
String buildQuery(String input) {
final queryBuilder = PrefixQueryBuilder.tokenize(
input: input,
minTokenLength: ftsMinTokenLength,
tokenLimit: ftsTokenLimit,
);
return queryBuilder.build();
}
}
mixin TrigramQueryBuilderMixin implements IQueryBuilder {
@override
String buildQuery(String input) {
final queryBuilder = TrigramQueryBuilder.tokenize(
input: input,
minTokenLength: ftsMinTokenLength,
tokenLimit: ftsTokenLimit,
);
return queryBuilder.build();
}
}
@@ -2,6 +2,8 @@
import 'dart:async';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dart';
import 'package:bang_navigator/features/search_browser/domain/services/create_tab.dart';
import 'package:flutter/widgets.dart';
@@ -65,3 +67,26 @@ class BottomSheetExtend extends _$BottomSheetExtend {
.sampleTime(const Duration(milliseconds: 50));
}
}
@Riverpod(keepAlive: true)
class SelectedBangTrigger extends _$SelectedBangTrigger {
void setTrigger(String trigger) {
state = trigger;
}
void clearTrigger() {
state = null;
}
@override
String? build() {
return null;
}
}
@Riverpod()
Stream<BangData?> selectedBangData(SelectedBangDataRef ref) {
final repository = ref.watch(bangRepositoryProvider.notifier);
final selectedBangTrigger = ref.watch(selectedBangTriggerProvider);
return repository.watchBang(selectedBangTrigger);
}
@@ -6,6 +6,21 @@ part of 'providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$selectedBangDataHash() => r'6d99a08f4a02027cc007ee1df30e5ac7923ca5f5';
/// See also [selectedBangData].
@ProviderFor(selectedBangData)
final selectedBangDataProvider = AutoDisposeStreamProvider<BangData?>.internal(
selectedBangData,
name: r'selectedBangDataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedBangDataHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SelectedBangDataRef = AutoDisposeStreamProviderRef<BangData?>;
String _$overlayDialogHash() => r'7a0376fff0ba12c70a257a47391e7c049dabd36d';
/// See also [OverlayDialog].
@@ -53,5 +68,22 @@ final bottomSheetExtendProvider =
);
typedef _$BottomSheetExtend = StreamNotifier<double>;
String _$selectedBangTriggerHash() =>
r'd110e4f8bd9d362e0cc54ee56a4f247d0870c760';
/// See also [SelectedBangTrigger].
@ProviderFor(SelectedBangTrigger)
final selectedBangTriggerProvider =
NotifierProvider<SelectedBangTrigger, String?>.internal(
SelectedBangTrigger.new,
name: r'selectedBangTriggerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedBangTriggerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SelectedBangTrigger = Notifier<String?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,93 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/data/models/bang_data.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangChips extends HookConsumerWidget {
static const _maxBangCount = 25;
final BangData? selectedBang;
final void Function(String trigger)? onSelected;
final void Function(String trigger)? onDeleted;
const BangChips({
this.selectedBang,
this.onSelected,
this.onDeleted,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final frequentBangsAsync = ref.watch(frequentBangsProvider);
return frequentBangsAsync.when(
data: (bangs) {
bangs = bangs.take(_maxBangCount).toList();
if (selectedBang != null) {
final selectedIndex =
bangs.indexWhere((bang) => bang.trigger == selectedBang?.trigger);
if (selectedIndex < 0) {
bangs = [
selectedBang!,
...bangs,
];
} else {
bangs = [
bangs.removeAt(selectedIndex),
...bangs,
];
}
}
return SizedBox(
height: 48,
child: Row(
children: [
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: bangs.length,
itemBuilder: (context, index) {
final bang = bangs[index];
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: FilterChip(
selected: bang.trigger == selectedBang?.trigger,
showCheckmark: false,
onSelected: (value) {
if (value) {
onSelected?.call(bang.trigger);
} else {
onDeleted?.call(bang.trigger);
}
},
onDeleted: () {
onDeleted?.call(bang.trigger);
},
avatar: BangIcon(bang, iconSize: 20),
label: Text(bang.websiteName),
),
);
},
),
),
IconButton(
onPressed: () async {
await context.push(BangRoute().location);
},
icon: const Icon(Icons.chevron_right),
),
],
),
);
},
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
);
}
}
@@ -1,9 +1,11 @@
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart';
import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:bang_navigator/features/kagi/domain/repositories/autosuggest.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/bang_chips.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart';
import 'package:bang_navigator/features/search_browser/presentation/widgets/speech_to_text_button.dart';
import 'package:bang_navigator/features/search_browser/utils/url_builder.dart'
as uri_builder;
import 'package:bang_navigator/features/share_intent/domain/entities/shared_content.dart';
import 'package:bang_navigator/presentation/widgets/autocomplete.dart';
import 'package:flutter/material.dart';
@@ -105,8 +107,16 @@ class SearchTab extends HookConsumerWidget {
final formKey = useMemoized(() => GlobalKey<FormState>());
final focusNode = useFocusNode();
final selectedBangAsync = ref.watch(selectedBangDataProvider);
final defaultSearchBangAsync = ref.watch(kagiSearchBangProvider);
final activeBang =
selectedBangAsync.valueOrNull ?? defaultSearchBangAsync.valueOrNull;
final textController =
useTextEditingController(text: sharedContent?.toString());
final quickAnswer = useListenableSelector(
textController,
() => textController.text.endsWith('?'),
@@ -118,6 +128,28 @@ class SearchTab extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
selectedBangAsync.when(
skipLoadingOnReload: true,
data: (selectedBang) {
return BangChips(
selectedBang: selectedBang,
onSelected: (trigger) {
ref
.read(selectedBangTriggerProvider.notifier)
.setTrigger(trigger);
},
onDeleted: (trigger) {
if (ref.read(selectedBangTriggerProvider) == trigger) {
ref
.read(selectedBangTriggerProvider.notifier)
.clearTrigger();
}
},
);
},
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => SizedBox.shrink(),
),
Consumer(
builder: (context, ref, child) {
final optionsStream = ref.watch(autosuggestRepositoryProvider);
@@ -169,7 +201,12 @@ class SearchTab extends HookConsumerWidget {
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
// border: OutlineInputBorder(),
prefixIcon: (activeBang != null)
? Padding(
padding: const EdgeInsetsDirectional.all(12.0),
child: BangIcon(activeBang, iconSize: 24.0),
)
: null,
label: const Text('Query'),
hintText: 'Ask anything...',
floatingLabelBehavior: FloatingLabelBehavior.always,
@@ -190,16 +227,15 @@ class SearchTab extends HookConsumerWidget {
onTapOutside: (event) {
focusNode.unfocus();
},
onFieldSubmitted: (value) {
if (formKey.currentState?.validate() ?? false) {
onSubmit(
uri_builder.searchUri(
searchQuery: textController.text,
),
);
}
onFieldSubmitted: (_) async {
if (activeBang != null &&
(formKey.currentState?.validate() == true)) {
await ref
.read(bangRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
// onFieldSubmitted();
onSubmit(activeBang.getUrl(textController.text));
}
},
);
},
@@ -226,13 +262,14 @@ class SearchTab extends HookConsumerWidget {
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
onSubmit(
uri_builder.searchUri(
searchQuery: textController.text,
),
);
onPressed: () async {
if (activeBang != null &&
(formKey.currentState?.validate() == true)) {
await ref
.read(bangRepositoryProvider.notifier)
.increaseFrequency(activeBang.trigger);
onSubmit(activeBang.getUrl(textController.text));
}
},
label: const Text('Search'),
@@ -36,6 +36,7 @@ Uri summarizerUri({
_baseUrl.replace(
pathSegments: [
'summarizer',
//TODO: check if really necessary
'index.html',
],
queryParameters: {
@@ -47,8 +48,3 @@ Uri summarizerUri({
_ => null,
},
);
Uri searchUri({required String searchQuery}) => _baseUrl.replace(
pathSegments: ['search'],
queryParameters: {'q': searchQuery},
);
@@ -1,13 +1,12 @@
import 'dart:async';
import 'package:bang_navigator/core/logger.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/chat_archive/domain/entities/chat_entity.dart';
import 'package:bang_navigator/features/chat_archive/domain/repositories/chat_archive.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/modes.dart';
import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dart';
import 'package:bang_navigator/features/search_browser/domain/providers.dart';
import 'package:bang_navigator/features/search_browser/utils/url_builder.dart'
as uri_builder;
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/features/web_view/domain/entities/web_view_page.dart';
import 'package:bang_navigator/features/web_view/presentation/controllers/switch_new_tab.dart';
@@ -158,13 +157,12 @@ class _WebViewState extends ConsumerState<WebView> {
await widget.page.value.controller?.getSelectedText();
if (selectedText != null && selectedText.isNotEmpty) {
final url = uri_builder.searchUri(
searchQuery: selectedText,
);
final searchBang =
await ref.read(kagiSearchBangProvider.future);
await ref
.read(switchNewTabControllerProvider.notifier)
.add(url);
.add(searchBang!.getUrl(selectedText));
}
},
),
+5
View File
@@ -1,6 +1,7 @@
import 'package:bang_navigator/core/logger.dart';
import 'package:bang_navigator/core/providers.dart';
import 'package:bang_navigator/features/about/data/repositories/package_info_repository.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart';
import 'package:bang_navigator/features/search_browser/domain/services/session.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/presentation/hooks/on_initialization.dart';
@@ -40,6 +41,10 @@ void main() async {
() async {
await ref.read(packageInfoProvider.future);
await ref
.read(bangRepositoryProvider.notifier)
.syncAllBangGroups();
final settings =
await ref.read(settingsRepositoryProvider.future);
+12
View File
@@ -0,0 +1,12 @@
import 'dart:typed_data';
import 'dart:ui';
Future<bool> isImageValid(Uint8List bytes) async {
try {
final codec = await instantiateImageCodec(bytes, targetWidth: 32);
final frameInfo = await codec.getNextFrame();
return frameInfo.image.width > 0;
} catch (e) {
return false;
}
}
@@ -7,12 +7,16 @@
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
sqlite3_flutter_libs
url_launcher_linux
)
@@ -11,6 +11,7 @@ import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import sqlite3_flutter_libs
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -20,5 +21,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
+65 -1
View File
@@ -137,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted
version: "1.3.1"
checked_yaml:
dependency: transitive
description:
@@ -273,6 +281,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
drift:
dependency: "direct main"
description:
name: drift
sha256: "6acedc562ffeed308049f78fb1906abad3d65714580b6745441ee6d50ec564cd"
url: "https://pub.dev"
source: hosted
version: "2.18.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: d9b020736ea85fff1568699ce18b89fabb3f0f042e8a7a05e84a3ec20d39acde
url: "https://pub.dev"
source: hosted
version: "2.18.0"
exceptions:
dependency: "direct main"
description:
@@ -644,13 +668,21 @@ packages:
source: hosted
version: "0.6.7"
json_annotation:
dependency: transitive
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
url: "https://pub.dev"
source: hosted
version: "6.8.0"
lint:
dependency: "direct dev"
description:
@@ -843,6 +875,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
riverpod:
dependency: "direct main"
description:
@@ -1032,6 +1072,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlite3:
dependency: "direct main"
description:
name: sqlite3
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
url: "https://pub.dev"
source: hosted
version: "2.4.3"
sqlite3_flutter_libs:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "9f89a7e7dc36eac2035808427eba1c3fbd79e59c3a22093d8dace6d36b1fe89e"
url: "https://pub.dev"
source: hosted
version: "0.5.23"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: ade9a67fd70d0369329ed3373208de7ebd8662470e8c396fc8d0d60f9acdfc9f
url: "https://pub.dev"
source: hosted
version: "0.36.0"
stack_trace:
dependency: transitive
description:
+6
View File
@@ -10,6 +10,7 @@ dependencies:
animations: ^2.0.11
collection: ^1.18.0
copy_with_extension: ^5.0.4
drift: ^2.18.0
exceptions: ^0.6.0
expandable_page_view: ^1.0.17
fast_equatable: ^1.1.0
@@ -28,6 +29,7 @@ dependencies:
hooks_riverpod: ^2.5.1
html: ^0.15.4
http: ^1.2.1
json_annotation: ^4.9.0
logger: ^2.2.0
markdown: ^7.2.2
mime: ^1.0.5
@@ -41,6 +43,8 @@ dependencies:
shared_preferences: ^2.2.3
skeletonizer: ^1.1.1
speech_to_text_google_dialog: ^0.0.6
sqlite3: ^2.4.3
sqlite3_flutter_libs: ^0.5.23
text_scroll: ^0.2.0
universal_io: ^2.2.2
uri_to_file: ^1.0.0
@@ -52,9 +56,11 @@ dev_dependencies:
copy_with_extension_gen: ^5.0.4
custom_lint: ^0.6.4
dependency_validator: ^4.1.0
drift_dev: ^2.18.0
fast_equatable_lint: ^0.3.0
flutter_launcher_icons: ^0.13.1
go_router_builder: ^2.4.1
json_serializable: ^6.8.0
lint: ^2.3.0
riverpod_generator: ^2.4.0
riverpod_lint: ^2.3.10
@@ -8,6 +8,7 @@
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_windows
share_plus
sqlite3_flutter_libs
url_launcher_windows
)