filter bangs by categories

This commit is contained in:
Fabian Freund
2024-06-16 06:32:55 +02:00
parent 2993ca59ea
commit 04e8dc7c0e
10 changed files with 660 additions and 98 deletions
@@ -90,4 +90,27 @@ bangQuery WITH BangData:
bang_icon bi ON b."trigger" = bi."trigger"
ORDER BY
RANK,
bf.frequency NULLS LAST;
bf.frequency NULLS LAST;
categoriesJson:
WITH categories AS (
SELECT
b.category,
json_group_array(
DISTINCT b.sub_category
ORDER BY b.sub_category
) AS sub_categories
FROM
bang b
WHERE
b.category IS NOT NULL AND
b.sub_category IS NOT NULL
GROUP BY b.category
ORDER BY b.category
)
SELECT
json_group_object(
c.category,
json(c.sub_categories)
) AS categories_json
FROM categories c;
@@ -1094,6 +1094,15 @@ abstract class _$BangDatabase extends GeneratedDatabase {
));
}
Selectable<String> categoriesJson() {
return customSelect(
'WITH categories AS (SELECT b.category, json_group_array(DISTINCT b.sub_category ORDER BY b.sub_category)AS sub_categories FROM bang AS b WHERE b.category IS NOT NULL AND b.sub_category IS NOT NULL GROUP BY b.category ORDER BY b.category) SELECT json_group_object(c.category, json(c.sub_categories)) AS categories_json FROM categories AS c',
variables: [],
readsFrom: {
bang,
}).map((QueryRow row) => row.read<String>('categories_json'));
}
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
+161 -9
View File
@@ -170,20 +170,172 @@ final kagiSearchBangDataProvider = StreamProvider<BangData?>.internal(
);
typedef KagiSearchBangDataRef = StreamProviderRef<BangData?>;
String _$bangDataListHash() => r'f5544ce7b3cc0f3e0fc3db25a99456c3cd080c1c';
String _$bangCategoriesHash() => r'dbc63b78208be90c2aa3c3d61b08f98818f4b89d';
/// See also [bangDataList].
@ProviderFor(bangDataList)
final bangDataListProvider = AutoDisposeStreamProvider<List<BangData>>.internal(
bangDataList,
name: r'bangDataListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$bangDataListHash,
/// See also [bangCategories].
@ProviderFor(bangCategories)
final bangCategoriesProvider =
AutoDisposeStreamProvider<Map<String, List<String>>>.internal(
bangCategories,
name: r'bangCategoriesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$bangCategoriesHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef BangDataListRef = AutoDisposeStreamProviderRef<List<BangData>>;
typedef BangCategoriesRef
= AutoDisposeStreamProviderRef<Map<String, List<String>>>;
String _$bangDataListHash() => r'64c3a3c55c481bf97f291f3befd8f958007daac6';
/// See also [bangDataList].
@ProviderFor(bangDataList)
const bangDataListProvider = BangDataListFamily();
/// See also [bangDataList].
class BangDataListFamily extends Family<AsyncValue<List<BangData>>> {
/// See also [bangDataList].
const BangDataListFamily();
/// See also [bangDataList].
BangDataListProvider call({
({
({String category, String? subCategory})? categoryFilter,
String? domain,
Iterable<BangGroup>? groups
})? filter,
}) {
return BangDataListProvider(
filter: filter,
);
}
@override
BangDataListProvider getProviderOverride(
covariant BangDataListProvider provider,
) {
return call(
filter: provider.filter,
);
}
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'bangDataListProvider';
}
/// See also [bangDataList].
class BangDataListProvider extends AutoDisposeStreamProvider<List<BangData>> {
/// See also [bangDataList].
BangDataListProvider({
({
({String category, String? subCategory})? categoryFilter,
String? domain,
Iterable<BangGroup>? groups
})? filter,
}) : this._internal(
(ref) => bangDataList(
ref as BangDataListRef,
filter: filter,
),
from: bangDataListProvider,
name: r'bangDataListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$bangDataListHash,
dependencies: BangDataListFamily._dependencies,
allTransitiveDependencies:
BangDataListFamily._allTransitiveDependencies,
filter: filter,
);
BangDataListProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.filter,
}) : super.internal();
final ({
({String category, String? subCategory})? categoryFilter,
String? domain,
Iterable<BangGroup>? groups
})? filter;
@override
Override overrideWith(
Stream<List<BangData>> Function(BangDataListRef provider) create,
) {
return ProviderOverride(
origin: this,
override: BangDataListProvider._internal(
(ref) => create(ref as BangDataListRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
filter: filter,
),
);
}
@override
AutoDisposeStreamProviderElement<List<BangData>> createElement() {
return _BangDataListProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BangDataListProvider && other.filter == filter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, filter.hashCode);
return _SystemHash.finish(hash);
}
}
mixin BangDataListRef on AutoDisposeStreamProviderRef<List<BangData>> {
/// The parameter `filter` of this provider.
({
({String category, String? subCategory})? categoryFilter,
String? domain,
Iterable<BangGroup>? groups
})? get filter;
}
class _BangDataListProviderElement
extends AutoDisposeStreamProviderElement<List<BangData>>
with BangDataListRef {
_BangDataListProviderElement(super.provider);
@override
({
({String category, String? subCategory})? categoryFilter,
String? domain,
Iterable<BangGroup>? groups
})? get filter => (origin as BangDataListProvider).filter;
}
String _$frequentBangDataListHash() =>
r'adf8d0b72adfa8faffadd7f1a294ce3945e22fec';
@@ -1,3 +1,5 @@
import 'dart:convert';
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';
@@ -27,6 +29,15 @@ class BangDataRepository extends _$BangDataRepository {
}
}
Stream<Map<String, List<String>>> watchCategories() {
return _db.categoriesJson().watchSingle().map((json) {
final decoded = jsonDecode(json) as Map<String, dynamic>;
return decoded.map(
(key, value) => MapEntry(key, (value as List<dynamic>).cast()),
);
});
}
Stream<int> watchBangCount(BangGroup group) {
return _db.bangDao.getBangCount(groups: [group]).watchSingle();
}
@@ -7,7 +7,7 @@ part of 'bang_data.dart';
// **************************************************************************
String _$bangDataRepositoryHash() =>
r'6ac3a59546f6d7e22f3c37aacd43a3579978eb50';
r'a6e4017a54516571af553426b0e8e3597d19f779';
/// See also [BangDataRepository].
@ProviderFor(BangDataRepository)
@@ -1,61 +0,0 @@
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(bangDataListProvider);
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,90 @@
import 'package:bang_navigator/core/routing/routes.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/presentation/widgets/failure_widget.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 BangCategoriesScreen extends HookConsumerWidget {
const BangCategoriesScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final categoriesAsync = ref.watch(bangCategoriesProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Bang Categories'),
actions: [
IconButton(
onPressed: () async {
await context.push(BangSearchRoute().location);
},
icon: const Icon(Icons.search),
),
],
),
body: categoriesAsync.when(
data: (categories) {
return SingleChildScrollView(
child: HookBuilder(
builder: (context) {
final expanded = useState(<String>{});
return ExpansionPanelList(
expansionCallback: (index, expand) {
final key = categories.keys.elementAt(index);
if (!expanded.value.contains(key)) {
expanded.value = {...expanded.value, key};
} else {
expanded.value = {...expanded.value}..remove(key);
}
},
children: categories.entries
.map(
(category) => ExpansionPanel(
canTapOnHeader: true,
isExpanded: expanded.value.contains(category.key),
headerBuilder: (context, isExpanded) => ListTile(
title: Text(category.key),
),
body: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: category.value
.map(
(subCategory) => ListTile(
title: Text(subCategory),
onTap: () async {
await context.push(
BangSubCategoryRoute(
category: category.key,
subCategory: subCategory,
).location,
);
},
),
)
.toList(),
),
),
),
)
.toList(),
);
},
),
);
},
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load Bang Categories',
exception: error,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
}
}
@@ -0,0 +1,77 @@
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:bang_navigator/presentation/widgets/failure_widget.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangListScreen extends HookConsumerWidget {
final String? category;
final String? subCategory;
const BangListScreen({this.category, this.subCategory, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bangsAsync = ref.watch(
bangDataListProvider(
filter: (
categoryFilter: (category != null)
? (category: category!, subCategory: subCategory)
: null,
domain: null,
groups: null
),
),
);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
title: Text('$category: $subCategory'),
),
bangsAsync.when(
data: (bangs) {
return SliverList.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) => Center(
child: FailureWidget(
title: 'Failed to load Bangs',
exception: error,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
],
),
);
}
}