refactored settings & added content blocking

This commit is contained in:
Fabian Freund
2024-06-19 21:09:54 +02:00
parent 2bacbbffd2
commit d3e30613c9
11 changed files with 385 additions and 215 deletions
@@ -1,3 +1,4 @@
import 'package:bang_navigator/features/content_block/data/models/host.dart';
import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
@@ -10,6 +11,8 @@ class Settings with FastEquatable {
final bool incognitoMode; final bool incognitoMode;
final bool enableJavascript; final bool enableJavascript;
final bool launchUrlExternal; final bool launchUrlExternal;
final bool enableContentBlocking;
final Set<HostSource> enableHostList;
Settings({ Settings({
required this.kagiSession, required this.kagiSession,
@@ -17,6 +20,8 @@ class Settings with FastEquatable {
required this.incognitoMode, required this.incognitoMode,
required this.enableJavascript, required this.enableJavascript,
required this.launchUrlExternal, required this.launchUrlExternal,
required this.enableContentBlocking,
required this.enableHostList,
}); });
Settings.withDefaults({ Settings.withDefaults({
@@ -25,10 +30,14 @@ class Settings with FastEquatable {
bool? incognitoMode, bool? incognitoMode,
bool? enableJavascript, bool? enableJavascript,
bool? launchUrlExternal, bool? launchUrlExternal,
bool? enableContentBlocking,
Set<HostSource>? enableHostList,
}) : showEarlyAccessFeatures = showEarlyAccessFeatures ?? true, }) : showEarlyAccessFeatures = showEarlyAccessFeatures ?? true,
incognitoMode = incognitoMode ?? true, incognitoMode = incognitoMode ?? true,
enableJavascript = enableJavascript ?? true, enableJavascript = enableJavascript ?? true,
launchUrlExternal = launchUrlExternal ?? false; launchUrlExternal = launchUrlExternal ?? false,
enableContentBlocking = enableContentBlocking ?? true,
enableHostList = enableHostList ?? {HostSource.stevenBlackUnified};
@override @override
bool get cacheHash => true; bool get cacheHash => true;
@@ -40,5 +49,7 @@ class Settings with FastEquatable {
incognitoMode, incognitoMode,
enableJavascript, enableJavascript,
launchUrlExternal, launchUrlExternal,
enableContentBlocking,
enableHostList,
]; ];
} }
@@ -17,6 +17,10 @@ abstract class _$SettingsCWProxy {
Settings launchUrlExternal(bool launchUrlExternal); Settings launchUrlExternal(bool launchUrlExternal);
Settings enableContentBlocking(bool enableContentBlocking);
Settings enableHostList(Set<HostSource> enableHostList);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
/// ///
/// Usage /// Usage
@@ -29,6 +33,8 @@ abstract class _$SettingsCWProxy {
bool? incognitoMode, bool? incognitoMode,
bool? enableJavascript, bool? enableJavascript,
bool? launchUrlExternal, bool? launchUrlExternal,
bool? enableContentBlocking,
Set<HostSource>? enableHostList,
}); });
} }
@@ -57,6 +63,14 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy {
Settings launchUrlExternal(bool launchUrlExternal) => Settings launchUrlExternal(bool launchUrlExternal) =>
this(launchUrlExternal: launchUrlExternal); this(launchUrlExternal: launchUrlExternal);
@override
Settings enableContentBlocking(bool enableContentBlocking) =>
this(enableContentBlocking: enableContentBlocking);
@override
Settings enableHostList(Set<HostSource> enableHostList) =>
this(enableHostList: enableHostList);
@override @override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
@@ -71,6 +85,8 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy {
Object? incognitoMode = const $CopyWithPlaceholder(), Object? incognitoMode = const $CopyWithPlaceholder(),
Object? enableJavascript = const $CopyWithPlaceholder(), Object? enableJavascript = const $CopyWithPlaceholder(),
Object? launchUrlExternal = const $CopyWithPlaceholder(), Object? launchUrlExternal = const $CopyWithPlaceholder(),
Object? enableContentBlocking = const $CopyWithPlaceholder(),
Object? enableHostList = const $CopyWithPlaceholder(),
}) { }) {
return Settings( return Settings(
kagiSession: kagiSession == const $CopyWithPlaceholder() kagiSession: kagiSession == const $CopyWithPlaceholder()
@@ -98,6 +114,17 @@ class _$SettingsCWProxyImpl implements _$SettingsCWProxy {
? _value.launchUrlExternal ? _value.launchUrlExternal
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: launchUrlExternal as bool, : launchUrlExternal as bool,
enableContentBlocking:
enableContentBlocking == const $CopyWithPlaceholder() ||
enableContentBlocking == null
? _value.enableContentBlocking
// ignore: cast_nullable_to_non_nullable
: enableContentBlocking as bool,
enableHostList: enableHostList == const $CopyWithPlaceholder() ||
enableHostList == null
? _value.enableHostList
// ignore: cast_nullable_to_non_nullable
: enableHostList as Set<HostSource>,
); );
} }
} }
@@ -1,4 +1,6 @@
import 'package:bang_navigator/features/content_block/data/models/host.dart';
import 'package:bang_navigator/features/settings/data/models/settings.dart'; import 'package:bang_navigator/features/settings/data/models/settings.dart';
import 'package:collection/collection.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -14,6 +16,8 @@ class SettingsRepository extends _$SettingsRepository {
static const _incognitoStorageKey = 'b4ng_settings_incognito'; static const _incognitoStorageKey = 'b4ng_settings_incognito';
static const _javascriptStorageKey = 'b4ng_settings_js'; static const _javascriptStorageKey = 'b4ng_settings_js';
static const _launchExternalStorageKey = 'b4ng_settings_launch_external'; static const _launchExternalStorageKey = 'b4ng_settings_launch_external';
static const _contentBlockingStorageKey = 'b4ng_settings_content_blocking';
static const _enableHostListStorageKey = 'b4ng_settings_host_lists';
final FlutterSecureStorage _flutterSecureStorage; final FlutterSecureStorage _flutterSecureStorage;
final Future<SharedPreferences> _sharedPreferences; final Future<SharedPreferences> _sharedPreferences;
@@ -23,6 +27,7 @@ class SettingsRepository extends _$SettingsRepository {
_sharedPreferences = SharedPreferences.getInstance(); _sharedPreferences = SharedPreferences.getInstance();
Future<void> updateSettings(UpdateSettingsFunc updateWithCurrent) async { Future<void> updateSettings(UpdateSettingsFunc updateWithCurrent) async {
final sharedPreferences = await _sharedPreferences;
final oldSettings = state.value!; final oldSettings = state.value!;
final newSettings = updateWithCurrent(oldSettings); final newSettings = updateWithCurrent(oldSettings);
@@ -38,32 +43,48 @@ class SettingsRepository extends _$SettingsRepository {
if (newSettings.showEarlyAccessFeatures != if (newSettings.showEarlyAccessFeatures !=
oldSettings.showEarlyAccessFeatures) { oldSettings.showEarlyAccessFeatures) {
await _sharedPreferences.then( await sharedPreferences.setBool(
(s) => s.setBool( _showEarlyAccessFeaturesKey,
_showEarlyAccessFeaturesKey, newSettings.showEarlyAccessFeatures,
newSettings.showEarlyAccessFeatures,
),
); );
} }
if (newSettings.incognitoMode != oldSettings.incognitoMode) { if (newSettings.incognitoMode != oldSettings.incognitoMode) {
await _sharedPreferences.then( await sharedPreferences.setBool(
(s) => s.setBool(_incognitoStorageKey, newSettings.incognitoMode), _incognitoStorageKey,
newSettings.incognitoMode,
); );
} }
if (newSettings.enableJavascript != oldSettings.enableJavascript) { if (newSettings.enableJavascript != oldSettings.enableJavascript) {
await _sharedPreferences.then( await sharedPreferences.setBool(
(s) => s.setBool(_javascriptStorageKey, newSettings.enableJavascript), _javascriptStorageKey,
newSettings.enableJavascript,
); );
} }
if (newSettings.launchUrlExternal != oldSettings.launchUrlExternal) { if (newSettings.launchUrlExternal != oldSettings.launchUrlExternal) {
await _sharedPreferences.then( await sharedPreferences.setBool(
(s) => s.setBool( _launchExternalStorageKey,
_launchExternalStorageKey, newSettings.launchUrlExternal,
newSettings.launchUrlExternal, );
), }
if (newSettings.enableContentBlocking !=
oldSettings.enableContentBlocking) {
await sharedPreferences.setBool(
_contentBlockingStorageKey,
newSettings.enableContentBlocking,
);
}
if (!const DeepCollectionEquality.unordered().equals(
newSettings.enableHostList,
oldSettings.enableHostList,
)) {
await sharedPreferences.setStringList(
_enableHostListStorageKey,
newSettings.enableHostList.map((list) => list.name).toList(),
); );
} }
@@ -82,6 +103,16 @@ class SettingsRepository extends _$SettingsRepository {
incognitoMode: sharedPreferences.getBool(_incognitoStorageKey), incognitoMode: sharedPreferences.getBool(_incognitoStorageKey),
enableJavascript: sharedPreferences.getBool(_javascriptStorageKey), enableJavascript: sharedPreferences.getBool(_javascriptStorageKey),
launchUrlExternal: sharedPreferences.getBool(_launchExternalStorageKey), launchUrlExternal: sharedPreferences.getBool(_launchExternalStorageKey),
enableContentBlocking:
sharedPreferences.getBool(_contentBlockingStorageKey),
enableHostList: sharedPreferences
.getStringList(_enableHostListStorageKey)
?.map(
(list) => HostSource.values
.firstWhereOrNull((source) => source.name == list),
)
.whereNotNull()
.toSet(),
); );
} }
} }
@@ -7,7 +7,7 @@ part of 'settings_repository.dart';
// ************************************************************************** // **************************************************************************
String _$settingsRepositoryHash() => String _$settingsRepositoryHash() =>
r'0beffd6449e5488e61beb181fefefd4aa24361b0'; r'9d0e8b8452f85ca33b8f13a4bbcb69212e7d29bd';
/// See also [SettingsRepository]. /// See also [SettingsRepository].
@ProviderFor(SettingsRepository) @ProviderFor(SettingsRepository)
@@ -1,12 +1,13 @@
import 'package:bang_navigator/core/extension/date_time.dart';
import 'package:bang_navigator/features/bangs/data/models/bang.dart'; import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart'; import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/data.dart'; import 'package:bang_navigator/features/bangs/domain/repositories/data.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/sync.dart'; import 'package:bang_navigator/features/content_block/data/models/host.dart';
import 'package:bang_navigator/features/settings/data/models/settings.dart'; import 'package:bang_navigator/features/settings/data/models/settings.dart';
import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart'; import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart';
import 'package:bang_navigator/features/settings/presentation/controllers/save_settings.dart'; import 'package:bang_navigator/features/settings/presentation/controllers/save_settings.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/button_list_tile.dart'; import 'package:bang_navigator/features/settings/presentation/widgets/bang_group_list_tile.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/host_list_tile.dart';
import 'package:bang_navigator/features/settings/utils/session_link_extractor.dart'; import 'package:bang_navigator/features/settings/utils/session_link_extractor.dart';
import 'package:bang_navigator/presentation/hooks/listenable_callback.dart'; import 'package:bang_navigator/presentation/hooks/listenable_callback.dart';
import 'package:bang_navigator/utils/ui_helper.dart' as ui_helper; import 'package:bang_navigator/utils/ui_helper.dart' as ui_helper;
@@ -31,39 +32,30 @@ class SettingsScreen extends HookConsumerWidget {
), ),
); );
Widget _buildSubSection(ThemeData theme, String name) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Text(
name,
style: theme.textTheme.titleLarge?.copyWith(fontSize: 18),
),
);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final kagiSessionTextController = useTextEditingController( final settings = ref.watch(
text: ref.read( settingsRepositoryProvider.select(
settingsRepositoryProvider (value) =>
.select((value) => value.valueOrNull?.kagiSession), value.valueOrNull ?? Settings.withDefaults(kagiSession: null),
), ),
); );
final kagiSessionTextController = useTextEditingController(
text: settings.kagiSession,
);
final hideSessionText = useState(true); final hideSessionText = useState(true);
final showEarlyAccessFeatures = ref.watch(
settingsRepositoryProvider.select(
(value) => value.valueOrNull?.showEarlyAccessFeatures ?? true,
),
);
final incognitoEnabled = ref.watch(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.incognitoMode ?? false),
);
final javacsriptEnabled = ref.watch(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.enableJavascript ?? false),
);
final launchUrlExternal = ref.watch(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.launchUrlExternal ?? false),
);
useListenableCallback(kagiSessionTextController, () async { useListenableCallback(kagiSessionTextController, () async {
var text = kagiSessionTextController.text; var text = kagiSessionTextController.text;
if (Uri.tryParse(text) case final Uri uri) { if (Uri.tryParse(text) case final Uri uri) {
@@ -142,7 +134,7 @@ class SettingsScreen extends HookConsumerWidget {
subtitle: const Text( subtitle: const Text(
"Displays Kagi's early access features in the UI. As an Ultimate subscriber, you will likely want to have this enabled.", "Displays Kagi's early access features in the UI. As an Ultimate subscriber, you will likely want to have this enabled.",
), ),
value: showEarlyAccessFeatures, value: settings.showEarlyAccessFeatures,
onChanged: (value) async { onChanged: (value) async {
await ref.read(saveSettingsControllerProvider.notifier).save( await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) => (currentSettings) =>
@@ -159,7 +151,7 @@ class SettingsScreen extends HookConsumerWidget {
subtitle: const Text( subtitle: const Text(
'Deletes all browsing data upon app restart for enhanced privacy.', 'Deletes all browsing data upon app restart for enhanced privacy.',
), ),
value: incognitoEnabled, value: settings.incognitoMode,
onChanged: (value) async { onChanged: (value) async {
await ref.read(saveSettingsControllerProvider.notifier).save( await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) => (currentSettings) =>
@@ -172,7 +164,7 @@ class SettingsScreen extends HookConsumerWidget {
subtitle: const Text( subtitle: const Text(
'While turning off JavaScript boosts security, privacy, and speed, it may cause some sites to not work as intended.', 'While turning off JavaScript boosts security, privacy, and speed, it may cause some sites to not work as intended.',
), ),
value: javacsriptEnabled, value: settings.enableJavascript,
onChanged: (value) async { onChanged: (value) async {
await ref.read(saveSettingsControllerProvider.notifier).save( await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) => (currentSettings) =>
@@ -185,7 +177,7 @@ class SettingsScreen extends HookConsumerWidget {
subtitle: const Text( subtitle: const Text(
'Opens all links (except for kagi.com) in your default browser.', 'Opens all links (except for kagi.com) in your default browser.',
), ),
value: launchUrlExternal, value: settings.launchUrlExternal,
onChanged: (value) async { onChanged: (value) async {
await ref.read(saveSettingsControllerProvider.notifier).save( await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) => (currentSettings) =>
@@ -196,11 +188,64 @@ class SettingsScreen extends HookConsumerWidget {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
_buildSection(theme, 'Content Blocking'),
SwitchListTile.adaptive(
title: const Text('Enable Content Blocking'),
subtitle: const Text(
'Prevents access to unwanted websites and ads, as defined in the selected lists below.',
),
value: settings.enableContentBlocking,
onChanged: (value) async {
await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) =>
currentSettings.copyWith.enableContentBlocking(value),
);
},
),
_buildSubSection(theme, 'Lists'),
HostListTile(
enableContentBlocking: settings.enableContentBlocking,
enableHostLists: settings.enableHostList,
source: HostSource.stevenBlackUnified,
title: 'StevenBlack: Unified',
subtitle: 'Blocks domains containing adware, malware and trackers.',
),
HostListTile(
enableContentBlocking: settings.enableContentBlocking,
enableHostLists: settings.enableHostList,
source: HostSource.stevenBlackFakeNews,
title: 'StevenBlack: Fake News',
subtitle: 'Blocks domains known for spreading fake news.',
),
HostListTile(
enableContentBlocking: settings.enableContentBlocking,
enableHostLists: settings.enableHostList,
source: HostSource.stevenBlackGambling,
title: 'StevenBlack: Gambling',
subtitle: 'Blocks domains related to gambling.',
),
HostListTile(
enableContentBlocking: settings.enableContentBlocking,
enableHostLists: settings.enableHostList,
source: HostSource.stevenBlackPorn,
title: 'StevenBlack: Porn',
subtitle: 'Blocks adult content domains.',
),
HostListTile(
enableContentBlocking: settings.enableContentBlocking,
enableHostLists: settings.enableHostList,
source: HostSource.stevenBlackSocial,
title: 'StevenBlack: Social',
subtitle: 'Blocks social media domains.',
),
const SizedBox(
height: 16,
),
_buildSection(theme, 'Bangs'), _buildSection(theme, 'Bangs'),
ButtonListTile( CustomListTile(
title: 'Bang Frequencies', title: 'Bang Frequencies',
subtitle: 'Tracked usage for Bang recommendations', subtitle: 'Tracked usage for Bang recommendations',
button: FilledButton.icon( suffix: FilledButton.icon(
onPressed: () async { onPressed: () async {
await ref await ref
.read(bangDataRepositoryProvider.notifier) .read(bangDataRepositoryProvider.notifier)
@@ -218,7 +263,7 @@ class SettingsScreen extends HookConsumerWidget {
), ),
); );
return ButtonListTile( return CustomListTile(
title: 'Icon Cache', title: 'Icon Cache',
subtitle: 'Stored favicons for Bangs', subtitle: 'Stored favicons for Bangs',
content: Padding( content: Padding(
@@ -226,6 +271,7 @@ class SettingsScreen extends HookConsumerWidget {
child: DefaultTextStyle( child: DefaultTextStyle(
style: GoogleFonts.robotoMono(), style: GoogleFonts.robotoMono(),
child: Table( child: Table(
columnWidths: const {0: FixedColumnWidth(100)},
children: [ children: [
TableRow( TableRow(
children: [ children: [
@@ -237,7 +283,7 @@ class SettingsScreen extends HookConsumerWidget {
), ),
), ),
), ),
button: FilledButton.icon( suffix: FilledButton.icon(
onPressed: () async { onPressed: () async {
await ref await ref
.read(bangDataRepositoryProvider.notifier) .read(bangDataRepositoryProvider.notifier)
@@ -249,164 +295,21 @@ class SettingsScreen extends HookConsumerWidget {
); );
}, },
), ),
Consumer( _buildSubSection(theme, 'Repositories'),
builder: (context, ref, child) { const BangGroupListTile(
final lastSync = ref.watch( group: BangGroup.general,
lastSyncOfGroupProvider(BangGroup.general).select( title: 'General Bangs',
(value) => value.valueOrNull, subtitle: 'Automatically syncs every 7 days',
),
);
final count = ref.watch(
bangCountOfGroupProvider(BangGroup.general).select(
(value) => value.valueOrNull,
),
);
return ButtonListTile(
title: 'General Bangs',
subtitle: 'Automatically syncs every 7 days',
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(),
child: Table(
children: [
TableRow(
children: [
const Text('Entries'),
Text(count?.toString() ?? 'N/A'),
],
),
TableRow(
children: [
const Text('Last Sync'),
Text(
lastSync?.formatWithMinutePrecision() ?? 'N/A',
),
],
),
],
),
),
),
button: FilledButton.icon(
onPressed: () async {
await ref
.read(bangSyncRepositoryProvider.notifier)
.syncGeneralBangs();
},
icon: const Icon(Icons.sync),
label: const Text('Sync'),
),
);
},
), ),
Consumer( const BangGroupListTile(
builder: (context, ref, child) { group: BangGroup.assistant,
final lastSync = ref.watch( title: 'Assistant Bangs',
lastSyncOfGroupProvider(BangGroup.assistant).select( subtitle: 'Automatically syncs every 7 days',
(value) => value.valueOrNull,
),
);
final count = ref.watch(
bangCountOfGroupProvider(BangGroup.assistant).select(
(value) => value.valueOrNull,
),
);
return ButtonListTile(
title: 'Assistant Bangs',
subtitle: 'Automatically syncs every 7 days',
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(),
child: Table(
children: [
TableRow(
children: [
const Text('Entries'),
Text(count?.toString() ?? 'N/A'),
],
),
TableRow(
children: [
const Text('Last Sync'),
Text(
lastSync?.formatWithMinutePrecision() ?? 'N/A',
),
],
),
],
),
),
),
button: FilledButton.icon(
onPressed: () async {
await ref
.read(bangSyncRepositoryProvider.notifier)
.syncAssistantBangs();
},
icon: const Icon(Icons.sync),
label: const Text('Sync'),
),
);
},
), ),
Consumer( const BangGroupListTile(
builder: (context, ref, child) { group: BangGroup.kagi,
final lastSync = ref.watch( title: 'Kagi Bangs',
lastSyncOfGroupProvider(BangGroup.kagi).select( subtitle: 'Automatically syncs every 7 days',
(value) => value.valueOrNull,
),
);
final count = ref.watch(
bangCountOfGroupProvider(BangGroup.kagi).select(
(value) => value.valueOrNull,
),
);
return ButtonListTile(
title: 'Kagi Bangs',
subtitle: 'Automatically syncs every 7 days',
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(),
child: Table(
children: [
TableRow(
children: [
const Text('Entries'),
Text(count?.toString() ?? 'N/A'),
],
),
TableRow(
children: [
const Text('Last Sync'),
Text(
lastSync?.formatWithMinutePrecision() ?? 'N/A',
),
],
),
],
),
),
),
button: FilledButton.icon(
onPressed: () async {
await ref
.read(bangSyncRepositoryProvider.notifier)
.syncKagiBangs();
},
icon: const Icon(Icons.sync),
label: const Text('Sync'),
),
);
},
), ),
], ],
), ),
@@ -0,0 +1,50 @@
import 'package:bang_navigator/features/bangs/data/models/bang.dart';
import 'package:bang_navigator/features/bangs/domain/providers.dart';
import 'package:bang_navigator/features/bangs/domain/repositories/sync.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/sync_details_table.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BangGroupListTile extends HookConsumerWidget {
final BangGroup group;
final String title;
final String subtitle;
const BangGroupListTile({
required this.group,
required this.title,
required this.subtitle,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final lastSync = ref.watch(
lastSyncOfGroupProvider(group).select((value) => value.valueOrNull),
);
final count = ref.watch(
bangCountOfGroupProvider(group).select((value) => value.valueOrNull),
);
return CustomListTile(
title: title,
subtitle: subtitle,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SyncDetailsTable(count, lastSync),
),
suffix: FilledButton.icon(
onPressed: () async {
await ref
.read(bangSyncRepositoryProvider.notifier)
.syncBangGroup(group, null);
},
icon: const Icon(Icons.sync),
label: const Text('Sync'),
),
);
}
}
@@ -1,18 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ButtonListTile extends StatelessWidget { class CustomListTile extends StatelessWidget {
final bool enabled;
final String title; final String title;
final String subtitle; final String subtitle;
final Widget? content; final Widget? content;
final Widget button; final Widget? prefix;
final Widget suffix;
const ButtonListTile({ const CustomListTile({
super.key, super.key,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
this.content, this.content,
required this.button, this.prefix,
required this.suffix,
this.enabled = true,
}); });
@override @override
@@ -23,6 +28,7 @@ class ButtonListTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row( child: Row(
children: [ children: [
if (prefix != null) prefix!,
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -30,17 +36,23 @@ class ButtonListTile extends StatelessWidget {
children: [ children: [
Text( Text(
title, title,
style: theme.textTheme.bodyLarge, style: enabled
? theme.textTheme.bodyLarge
: theme.textTheme.bodyLarge
?.copyWith(color: theme.disabledColor),
), ),
Text( Text(
subtitle, subtitle,
style: theme.textTheme.bodyMedium, style: enabled
? theme.textTheme.bodyMedium
: theme.textTheme.bodyMedium
?.copyWith(color: theme.disabledColor),
), ),
if (content != null) content!, if (content != null) content!,
], ],
), ),
), ),
button, suffix,
], ],
), ),
); );
@@ -0,0 +1,83 @@
import 'package:bang_navigator/features/content_block/data/models/host.dart';
import 'package:bang_navigator/features/content_block/domain/providers.dart';
import 'package:bang_navigator/features/content_block/domain/repositories/sync.dart';
import 'package:bang_navigator/features/settings/data/models/settings.dart';
import 'package:bang_navigator/features/settings/presentation/controllers/save_settings.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:bang_navigator/features/settings/presentation/widgets/sync_details_table.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class HostListTile extends HookConsumerWidget {
final bool enabled;
final bool enableContentBlocking;
final Set<HostSource> enableHostLists;
final HostSource source;
final String title;
final String subtitle;
const HostListTile({
required this.enableContentBlocking,
required this.enableHostLists,
required this.source,
required this.title,
required this.subtitle,
this.enabled = true,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final enabled = enableHostLists.contains(source);
final lastSync = ref.watch(
lastSyncOfSourceProvider(source).select((value) => value.valueOrNull),
);
final count = ref.watch(
hostCountOfSourceProvider(source).select((value) => value.valueOrNull),
);
Future<void> toggleHostLists(HostSource source) async {
final lists = enabled
? ({...enableHostLists}..remove(source))
: {...enableHostLists, source};
await ref.read(saveSettingsControllerProvider.notifier).save(
(currentSettings) => currentSettings.copyWith.enableHostList(lists),
);
}
return CustomListTile(
enabled: enableContentBlocking,
title: title,
subtitle: subtitle,
prefix: Checkbox.adaptive(
value: enableHostLists.contains(source),
onChanged: enableContentBlocking
? (_) async {
await toggleHostLists(source);
}
: null,
),
suffix: FilledButton.icon(
onPressed: (enabled && enableContentBlocking)
? () async {
await ref
.read(hostSyncRepositoryProvider.notifier)
.syncHostSource(source, null);
}
: null,
icon: const Icon(Icons.sync),
label: const Text('Sync'),
),
content: (enabled && enableContentBlocking)
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SyncDetailsTable(count, lastSync),
)
: null,
);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:timeago/timeago.dart' as timeago;
class SyncDetailsTable extends StatelessWidget {
final int? count;
final DateTime? lastSync;
const SyncDetailsTable(this.count, this.lastSync);
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: GoogleFonts.robotoMono(),
child: Table(
columnWidths: const {0: FixedColumnWidth(100)},
children: [
TableRow(
children: [
const Text('Entries'),
Text(count?.toString() ?? 'N/A'),
],
),
TableRow(
children: [
const Text('Last Sync'),
Text(
(lastSync != null) ? timeago.format(lastSync!) : 'N/A',
),
],
),
],
),
);
}
}
+16
View File
@@ -680,6 +680,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.2.0"
intl:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -1213,6 +1221,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
timeago:
dependency: "direct main"
description:
name: timeago
sha256: d3204eb4c788214883380253da7f23485320a58c11d145babc82ad16bf4e7764
url: "https://pub.dev"
source: hosted
version: "3.6.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
+1
View File
@@ -48,6 +48,7 @@ dependencies:
sqlite3: ^2.4.3 sqlite3: ^2.4.3
sqlite3_flutter_libs: ^0.5.23 sqlite3_flutter_libs: ^0.5.23
text_scroll: ^0.2.0 text_scroll: ^0.2.0
timeago: ^3.6.1
universal_io: ^2.2.2 universal_io: ^2.2.2
uri_to_file: ^1.0.0 uri_to_file: ^1.0.0
url_launcher: ^6.2.5 url_launcher: ^6.2.5