prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/bangs/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/sync_details_table.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.value),
|
||||
);
|
||||
|
||||
final count = ref.watch(
|
||||
bangCountOfGroupProvider(group).select((value) => value.value),
|
||||
);
|
||||
|
||||
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)
|
||||
.syncRemoteBangGroup(group, null);
|
||||
},
|
||||
icon: const Icon(Icons.sync),
|
||||
label: const Text('Sync'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
|
||||
class BangIcon extends ConsumerWidget {
|
||||
final BangKey trigger;
|
||||
final double iconSize;
|
||||
|
||||
const BangIcon({super.key, required this.trigger, this.iconSize = 20});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bang = ref.watch(
|
||||
bangDataProvider(trigger).select((value) => value.value),
|
||||
);
|
||||
|
||||
return UrlIcon([
|
||||
if (bang != null) bang.getDefaultUrl(),
|
||||
], iconSize: iconSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomListTile extends StatelessWidget {
|
||||
final bool enabled;
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget? content;
|
||||
|
||||
final Widget? prefix;
|
||||
final Widget? suffix;
|
||||
|
||||
const CustomListTile({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.content,
|
||||
this.prefix,
|
||||
this.suffix,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final subtitleTextTheme = theme.textTheme.bodyMedium!.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
if (prefix != null) prefix!,
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: enabled
|
||||
? theme.textTheme.bodyLarge
|
||||
: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: enabled
|
||||
? subtitleTextTheme
|
||||
: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
),
|
||||
if (content != null) content!,
|
||||
],
|
||||
),
|
||||
),
|
||||
if (suffix != null) suffix!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
|
||||
class DefaultSearchSelector extends HookConsumerWidget {
|
||||
const DefaultSearchSelector({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activeBang = ref.watch(
|
||||
defaultSearchBangDataProvider.select((value) => value.value),
|
||||
);
|
||||
final availableBangs = ref.watch(frequentBangListProvider);
|
||||
|
||||
Future<void> updateSearchProvider(BangKey key) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.defaultSearchProvider(key),
|
||||
);
|
||||
}
|
||||
|
||||
return availableBangs.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (availableBangs) {
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
itemId: (bang) => bang.trigger,
|
||||
itemAvatar: (bang) =>
|
||||
UrlIcon([bang.getDefaultUrl()], iconSize: 20),
|
||||
itemLabel: (bang) => Text(bang.websiteName),
|
||||
itemTooltip: (bang) => bang.trigger,
|
||||
availableItems: availableBangs,
|
||||
selectedItem: activeBang,
|
||||
onSelected: (bang) async {
|
||||
await updateSearchProvider(bang.toKey());
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final trigger = await const BangSearchRoute().push<BangKey?>(
|
||||
context,
|
||||
);
|
||||
|
||||
if (trigger != null) {
|
||||
await updateSearchProvider(trigger);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Center(
|
||||
child: Icon(Icons.error, color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
loading: () => const SizedBox(height: 48, width: double.infinity),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||
import 'package:weblibre/utils/form_validators.dart';
|
||||
|
||||
class DohSettingsContent extends HookConsumerWidget {
|
||||
const DohSettingsContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
|
||||
final dohSettings = ref.watch(
|
||||
engineSettingsWithDefaultsProvider.select((value) => value.dohSettings),
|
||||
);
|
||||
|
||||
final customProviderController = useTextEditingController(
|
||||
text: BuiltInDohProviders.isBuiltin(dohSettings.dohProviderUrl)
|
||||
? null
|
||||
: dohSettings.dohProviderUrl,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(MdiIcons.dns),
|
||||
title: Text('Protection Level'),
|
||||
subtitle: Text(
|
||||
'Domain Name System (DNS) over HTTPS sends your request for a domain name through an encrypted connection, providing a secure DNS and making it harder for others to see which web site you\u2019re about to access.',
|
||||
),
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: dohSettings.dohSettingsMode,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref
|
||||
.read(saveEngineSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.dohSettingsMode(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Column(
|
||||
children: [
|
||||
RadioListTile.adaptive(
|
||||
value: DohSettingsMode.geckoDefault,
|
||||
title: Text('Default Protection'),
|
||||
subtitle: Text('DoH used only when default DNS fails'),
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: DohSettingsMode.increased,
|
||||
title: Text('Increased Protection'),
|
||||
subtitle: Text('DoH preferred, default DNS as fallback'),
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: DohSettingsMode.max,
|
||||
title: Text('Max Protection'),
|
||||
subtitle: Text('DoH only, no fallback'),
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: DohSettingsMode.off,
|
||||
title: Text('Off'),
|
||||
subtitle: Text('Use your default DNS resolver'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const ListTile(
|
||||
leading: Icon(MdiIcons.routerNetwork),
|
||||
title: Text('DoH Provider'),
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: dohSettings.dohProviderUrl,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref
|
||||
.read(saveEngineSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.dohProviderUrl(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: BuiltInDohProviders.values
|
||||
.map(
|
||||
(provider) => RadioListTile.adaptive(
|
||||
value: provider.url,
|
||||
title: Text(provider.name),
|
||||
subtitle: Text(provider.url),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: !BuiltInDohProviders.isBuiltin(
|
||||
dohSettings.dohProviderUrl,
|
||||
),
|
||||
onChanged: (value) {},
|
||||
child: RadioListTile(
|
||||
value: true,
|
||||
enabled: false,
|
||||
title: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
controller: customProviderController,
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: const InputDecoration(
|
||||
label: Text('Custom Resolver URL'),
|
||||
hintText: 'https://example.com/dns-query',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
validator: (value) {
|
||||
return validateUrl(
|
||||
value,
|
||||
onlyHttpProtocol: true,
|
||||
eagerParsing: false,
|
||||
);
|
||||
},
|
||||
onSaved: (newProvider) async {
|
||||
if (newProvider != null) {
|
||||
await ref
|
||||
.read(saveEngineSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.dohProviderUrl(newProvider),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
if (formKey.currentState?.validate() == true) {
|
||||
formKey.currentState?.save();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
|
||||
class HardeningGroupIcon extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final bool isPartlyActive;
|
||||
|
||||
const HardeningGroupIcon({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
this.isPartlyActive = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isActive) {
|
||||
return Icon(
|
||||
MdiIcons.shieldLockOutline,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
);
|
||||
} else if (isPartlyActive) {
|
||||
return Icon(
|
||||
MdiIcons.shieldLockOpenOutline,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
);
|
||||
}
|
||||
|
||||
return Icon(
|
||||
MdiIcons.shieldOffOutline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingSection extends StatelessWidget {
|
||||
final String name;
|
||||
|
||||
const SettingSection({super.key, required this.name});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: Theme.of(context).textTheme.titleLarge),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingSubSection extends StatelessWidget {
|
||||
final String name;
|
||||
|
||||
const SettingSubSection({super.key, required this.name});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
child: Text(
|
||||
name,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTextStyle(
|
||||
style: GoogleFonts.robotoMono(
|
||||
textStyle: DefaultTextStyle.of(context).style,
|
||||
),
|
||||
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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class ToolbarLayoutContent extends StatelessWidget {
|
||||
const ToolbarLayoutContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
children: [
|
||||
SettingSection(name: 'Tab Bar'),
|
||||
_TabBarPositionSection(),
|
||||
_TabBarLayoutModeSection(),
|
||||
_AutoHideTabBarTile(),
|
||||
_TabBarLongPressUrlCopyTile(),
|
||||
SettingSection(name: 'Contextual Toolbar'),
|
||||
_ShowContextualTabBarTile(),
|
||||
_CustomizeToolbarButtonsTile(),
|
||||
SettingSection(name: 'Quick Tab Switcher'),
|
||||
_ShowQuickTabSwitcherBarTile(),
|
||||
_QuickTabSwitcherModeSection(),
|
||||
_QuickTabSwitcherHistorySuggestionsTile(),
|
||||
_QuickTabSwitcherShowTitlesTile(),
|
||||
SettingSection(name: 'Tab View'),
|
||||
_BottomSheetTabViewTile(),
|
||||
_TabListShowFaviconsTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarPositionSection extends HookConsumerWidget {
|
||||
const _TabBarPositionSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarPosition = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.tabBarPosition),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('Tab Bar Position'),
|
||||
leading: Icon(MdiIcons.dockWindow),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: tabBarPosition,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabBarPosition(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Column(
|
||||
children: [
|
||||
RadioListTile.adaptive(
|
||||
value: TabBarPosition.top,
|
||||
title: Text('Top'),
|
||||
subtitle: Text('Persistent tab bar without auto-hide'),
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: TabBarPosition.bottom,
|
||||
title: Text('Bottom'),
|
||||
subtitle: Text('Tab bar with auto-hide support'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarLayoutModeSection extends HookConsumerWidget {
|
||||
const _TabBarLayoutModeSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarLayout = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.tabBarLayout),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('Tab Bar Style'),
|
||||
leading: Icon(MdiIcons.tabUnselected),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: tabBarLayout,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabBarLayout(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Column(
|
||||
children: [
|
||||
RadioListTile.adaptive(
|
||||
value: TabBarLayout.withTitle,
|
||||
title: Text('With Title'),
|
||||
subtitle: Text('Shows page title and URL breadcrumb'),
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: TabBarLayout.compact,
|
||||
title: Text('Compact'),
|
||||
subtitle: Text('Centered URL pill without page title'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShowContextualTabBarTile extends HookConsumerWidget {
|
||||
const _ShowContextualTabBarTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarShowContextualBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarShowContextualBar,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Show Contextual Toolbar'),
|
||||
subtitle: const Text(
|
||||
'Show additional bottom toolbar for navigation and actions',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.dockBottom),
|
||||
value: tabBarShowContextualBar,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabBarShowContextualBar(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomizeToolbarButtonsTile extends HookConsumerWidget {
|
||||
const _CustomizeToolbarButtonsTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarShowContextualBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarShowContextualBar,
|
||||
),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.tune),
|
||||
title: const Text('Customize Toolbar Buttons'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
enabled: tabBarShowContextualBar,
|
||||
onTap: () async {
|
||||
await const ContextualToolbarSettingsRoute().push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShowQuickTabSwitcherBarTile extends HookConsumerWidget {
|
||||
const _ShowQuickTabSwitcherBarTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarShowQuickTabSwitcherBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarShowQuickTabSwitcherBar,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Show Quick Tab Switcher Bar'),
|
||||
subtitle: const Text(
|
||||
'Show additional toolbar to quickly switch to recently used tabs',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.dockBottom),
|
||||
value: tabBarShowQuickTabSwitcherBar,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabBarShowQuickTabSwitcherBar(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickTabSwitcherModeSection extends HookConsumerWidget {
|
||||
const _QuickTabSwitcherModeSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
final quickTabSwitcherMode = settings.effectiveUiQuickTabSwitcherMode();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('Quick Tab Switcher Mode'),
|
||||
leading: Icon(MdiIcons.folderSettings),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
RadioGroup(
|
||||
groupValue: quickTabSwitcherMode,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.quickTabSwitcherMode(value),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
const RadioListTile.adaptive(
|
||||
value: QuickTabSwitcherMode.lastUsedTabs,
|
||||
title: Text('Recently Used Tabs'),
|
||||
subtitle: Text('Recently used tabs across all containers'),
|
||||
),
|
||||
if (settings.showContainerUi)
|
||||
const RadioListTile.adaptive(
|
||||
value: QuickTabSwitcherMode.containerTabs,
|
||||
title: Text('Container Tabs'),
|
||||
subtitle: Text('Ordered tabs of the selected container'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickTabSwitcherHistorySuggestionsTile extends HookConsumerWidget {
|
||||
const _QuickTabSwitcherHistorySuggestionsTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final showHistorySuggestions = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.quickTabSwitcherShowHistorySuggestions,
|
||||
),
|
||||
);
|
||||
final tabBarShowQuickTabSwitcherBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarShowQuickTabSwitcherBar,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('History Fallback in Quick Tab Switcher'),
|
||||
subtitle: const Text(
|
||||
'Use browsing history suggestions when no tab chips are available',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.history),
|
||||
value: showHistorySuggestions,
|
||||
onChanged: tabBarShowQuickTabSwitcherBar
|
||||
? (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.quickTabSwitcherShowHistorySuggestions(value),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickTabSwitcherShowTitlesTile extends HookConsumerWidget {
|
||||
const _QuickTabSwitcherShowTitlesTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final quickTabSwitcherShowTitles = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.quickTabSwitcherShowTitles,
|
||||
),
|
||||
);
|
||||
final tabBarShowQuickTabSwitcherBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarShowQuickTabSwitcherBar,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Show Titles in Quick Tab Switcher'),
|
||||
subtitle: const Text(
|
||||
'Display tab titles alongside icons in the quick tab switcher bar',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.textRecognition),
|
||||
value: quickTabSwitcherShowTitles,
|
||||
onChanged: tabBarShowQuickTabSwitcherBar
|
||||
? (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.quickTabSwitcherShowTitles(value),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoHideTabBarTile extends HookConsumerWidget {
|
||||
const _AutoHideTabBarTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final autoHideTabBar = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.autoHideTabBar),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Auto Hide Tab Bar'),
|
||||
subtitle: const Text('Hide tab bar when scrolling'),
|
||||
secondary: const Icon(MdiIcons.folderHidden),
|
||||
value: autoHideTabBar,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.autoHideTabBar(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomSheetTabViewTile extends HookConsumerWidget {
|
||||
const _BottomSheetTabViewTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabViewBottomSheet = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.tabViewBottomSheet),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Bottom Sheet Tab View'),
|
||||
subtitle: const Text(
|
||||
'Display tabs in a bottom sheet instead of fullscreen',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.dockBottom),
|
||||
value: tabViewBottomSheet,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabViewBottomSheet(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabBarLongPressUrlCopyTile extends HookConsumerWidget {
|
||||
const _TabBarLongPressUrlCopyTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabBarLongPressUrlCopy = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.tabBarLongPressUrlCopy,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Long Press URL to Copy'),
|
||||
subtitle: const Text(
|
||||
'Copy the page URL to clipboard when long pressing the address bar',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.contentCopy),
|
||||
value: tabBarLongPressUrlCopy,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabBarLongPressUrlCopy(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabListShowFaviconsTile extends HookConsumerWidget {
|
||||
const _TabListShowFaviconsTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabListShowFavicons = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.tabListShowFavicons),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Show Favicons in List View'),
|
||||
subtitle: const Text(
|
||||
'Display website icons instead of page thumbnails in tab list view',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.web),
|
||||
value: tabListShowFavicons,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.tabListShowFavicons(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
|
||||
class TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
const TabBarPreviewHeaderDelegate({
|
||||
required this.settings,
|
||||
this.backgroundColor,
|
||||
this.compact = false,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
});
|
||||
|
||||
static const _kPreviewBaseHeight = 90.0;
|
||||
static const _kCompactPreviewBaseHeight = 42.0;
|
||||
static const _kHeaderHeight = 72.0;
|
||||
|
||||
final GeneralSettings settings;
|
||||
final Color? backgroundColor;
|
||||
final bool compact;
|
||||
final EdgeInsets padding;
|
||||
|
||||
double get _toolbarHeight {
|
||||
var height = kToolbarHeight;
|
||||
|
||||
if (settings.tabBarShowContextualBar) {
|
||||
height += BrowserTabBar.contextualToolabarHeight;
|
||||
}
|
||||
|
||||
if (settings.tabBarShowQuickTabSwitcherBar) {
|
||||
height += BrowserTabBar.quickTabSwitcherHeight;
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
double get _baseHeight =>
|
||||
compact ? _kCompactPreviewBaseHeight : _kPreviewBaseHeight;
|
||||
|
||||
double get _headerHeight => compact ? 0.0 : _kHeaderHeight;
|
||||
|
||||
@override
|
||||
double get minExtent =>
|
||||
_baseHeight + _toolbarHeight + _headerHeight + padding.vertical;
|
||||
|
||||
@override
|
||||
double get maxExtent =>
|
||||
_baseHeight + _toolbarHeight + _headerHeight + padding.vertical;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return ColoredBox(
|
||||
color: backgroundColor ?? Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: TabBarPreviewCard(settings: settings, compact: compact),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant TabBarPreviewHeaderDelegate oldDelegate) {
|
||||
return oldDelegate.settings != settings ||
|
||||
oldDelegate.backgroundColor != backgroundColor ||
|
||||
oldDelegate.compact != compact;
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarPreviewCard extends HookWidget {
|
||||
const TabBarPreviewCard({
|
||||
super.key,
|
||||
required this.settings,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final GeneralSettings settings;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final quickTabsController = useScrollController();
|
||||
final showMainToolbarActionButtons = !settings.tabBarShowContextualBar;
|
||||
|
||||
final previewTabState = TabState.$default('preview-tab').copyWith(
|
||||
url: Uri.parse('https://weblibre.eu/docs'),
|
||||
title: 'WebLibre Preview',
|
||||
securityInfoState: SecurityState(
|
||||
secure: true,
|
||||
host: 'weblibre.eu',
|
||||
issuer: 'WebLibre',
|
||||
),
|
||||
);
|
||||
|
||||
final previewQuickItems = <QuickTabSwitcherItem>[
|
||||
QuickTabSwitcherItem(
|
||||
id: 'regular-preview-tab',
|
||||
isActive: true,
|
||||
title: 'News',
|
||||
tabMode: TabMode.regular,
|
||||
isHistory: false,
|
||||
isPinned:
|
||||
settings.effectiveUiQuickTabSwitcherMode() ==
|
||||
QuickTabSwitcherMode.containerTabs,
|
||||
url: Uri.parse('https://example.com/news'),
|
||||
color: settings.showContainerUi
|
||||
? colorScheme.primary.withValues(alpha: 0.18)
|
||||
: null,
|
||||
avatar: const Icon(MdiIcons.web, size: 20),
|
||||
),
|
||||
QuickTabSwitcherItem(
|
||||
id: 'private-preview-tab',
|
||||
isActive: false,
|
||||
title: 'Private',
|
||||
tabMode: TabMode.private,
|
||||
isHistory: false,
|
||||
isPinned: false,
|
||||
url: Uri.parse('https://example.com/private'),
|
||||
color: null,
|
||||
avatar: const Icon(MdiIcons.web, size: 20),
|
||||
),
|
||||
if (settings.showIsolatedTabUi)
|
||||
QuickTabSwitcherItem(
|
||||
id: 'isolated-preview-tab',
|
||||
isActive: false,
|
||||
title: 'Bank',
|
||||
tabMode: TabMode.isolated('preview-isolated-context'),
|
||||
isHistory: false,
|
||||
isPinned: false,
|
||||
url: Uri.parse('https://example.com/bank'),
|
||||
color: null,
|
||||
avatar: const Icon(MdiIcons.web, size: 20),
|
||||
),
|
||||
if (settings.quickTabSwitcherShowHistorySuggestions)
|
||||
QuickTabSwitcherItem(
|
||||
id: 'history-preview-tab',
|
||||
isActive: false,
|
||||
title: 'Search',
|
||||
tabMode: TabMode.regular,
|
||||
isHistory: true,
|
||||
isPinned: false,
|
||||
url: Uri.parse('https://search.example.com'),
|
||||
color: null,
|
||||
avatar: const Icon(MdiIcons.web, size: 20),
|
||||
),
|
||||
];
|
||||
|
||||
final tabCountButton = TabsCountButtonView(
|
||||
isActive: false,
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
buttonBuilder: (isActive, onTap, onLongPress) {
|
||||
return TabsActionButtonView(
|
||||
isActive: isActive,
|
||||
tabCountText: '5',
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget buildQuickTabSwitcher() {
|
||||
return QuickTabSwitcherView(
|
||||
availableItems: previewQuickItems,
|
||||
activeItem: previewQuickItems.firstWhere((item) => item.isActive),
|
||||
scrollController: quickTabsController,
|
||||
showTitles: settings.quickTabSwitcherShowTitles,
|
||||
showIsolatedTabUi: settings.showIsolatedTabUi,
|
||||
onSelected: (_) async {},
|
||||
itemWrapBuilder: (child, _) => child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildContextualToolbar() {
|
||||
return ContextualToolbarView(
|
||||
buttons: [
|
||||
NavigateBackButtonView(
|
||||
canGoBack: true,
|
||||
isLoading: false,
|
||||
onPressed: () {},
|
||||
onLongPress: () {},
|
||||
),
|
||||
NavigateForwardButtonView(
|
||||
canGoForward: true,
|
||||
onPressed: () {},
|
||||
onLongPress: () {},
|
||||
),
|
||||
AddTabButtonView(onPressed: () {}, onLongPress: () {}),
|
||||
tabCountButton,
|
||||
NavigationMenuButtonView(onTap: () {}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final mainToolbarActions = <Widget>[
|
||||
if (showMainToolbarActionButtons) tabCountButton,
|
||||
if (showMainToolbarActionButtons) NavigationMenuButtonView(onTap: () {}),
|
||||
];
|
||||
|
||||
final bottomCombinedToolbar = BrowserTabBarView(
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: settings.tabBarShowContextualBar,
|
||||
showQuickTabSwitcherBar: settings.tabBarShowQuickTabSwitcherBar,
|
||||
displayAppBar: true,
|
||||
displayQuickTabSwitcher: true,
|
||||
backgroundColor: settings.showContainerUi
|
||||
? colorScheme.primaryContainer.withValues(alpha: 0.55)
|
||||
: colorScheme.surfaceContainer,
|
||||
title: settings.tabBarLayout == TabBarLayout.compact
|
||||
? _CompactPreviewTitle(tabState: previewTabState)
|
||||
: _RegularPreviewTitle(tabState: previewTabState),
|
||||
actions: mainToolbarActions,
|
||||
quickTabSwitcher: buildQuickTabSwitcher(),
|
||||
contextualToolbar: buildContextualToolbar(),
|
||||
);
|
||||
|
||||
final topMainToolbar = BrowserTabBarView(
|
||||
showMainToolbar: true,
|
||||
showContextualToolbar: false,
|
||||
showQuickTabSwitcherBar: false,
|
||||
displayAppBar: true,
|
||||
displayQuickTabSwitcher: false,
|
||||
backgroundColor: settings.showContainerUi
|
||||
? colorScheme.primaryContainer.withValues(alpha: 0.55)
|
||||
: colorScheme.surfaceContainer,
|
||||
title: settings.tabBarLayout == TabBarLayout.compact
|
||||
? _CompactPreviewTitle(tabState: previewTabState)
|
||||
: _RegularPreviewTitle(tabState: previewTabState),
|
||||
actions: mainToolbarActions,
|
||||
quickTabSwitcher: const SizedBox.shrink(),
|
||||
contextualToolbar: const SizedBox.shrink(),
|
||||
);
|
||||
|
||||
final topBottomToolbar = BrowserTabBarView(
|
||||
showMainToolbar: false,
|
||||
showContextualToolbar: settings.tabBarShowContextualBar,
|
||||
showQuickTabSwitcherBar: settings.tabBarShowQuickTabSwitcherBar,
|
||||
displayAppBar: false,
|
||||
displayQuickTabSwitcher: true,
|
||||
backgroundColor: colorScheme.surfaceContainer,
|
||||
title: null,
|
||||
actions: const [],
|
||||
quickTabSwitcher: buildQuickTabSwitcher(),
|
||||
contextualToolbar: buildContextualToolbar(),
|
||||
);
|
||||
|
||||
final previewContent = Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: compact
|
||||
? colorScheme.surface.withValues(alpha: 0.7)
|
||||
: colorScheme.surface,
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (settings.tabBarPosition == TabBarPosition.top) topMainToolbar,
|
||||
Container(
|
||||
height: compact ? 40 : 72,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: compact
|
||||
? colorScheme.surfaceContainerLowest.withValues(alpha: 0.7)
|
||||
: colorScheme.surfaceContainerLowest,
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Page Content',
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
),
|
||||
if (settings.tabBarPosition == TabBarPosition.top)
|
||||
topBottomToolbar
|
||||
else
|
||||
bottomCombinedToolbar,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return previewContent;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Text('Live Preview'),
|
||||
subtitle: Text(
|
||||
'Reflects your current toolbar and layout settings',
|
||||
),
|
||||
leading: Icon(MdiIcons.televisionGuide),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
),
|
||||
previewContent,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegularPreviewTitle extends StatelessWidget {
|
||||
const _RegularPreviewTitle({required this.tabState});
|
||||
|
||||
final TabState tabState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBarTitleView(
|
||||
tabState: tabState,
|
||||
isTabTunneled: false,
|
||||
showSiteSettingsBadge: false,
|
||||
onSiteSettingsTap: _noop,
|
||||
onTitleTap: _noop,
|
||||
tabIcon: const Icon(MdiIcons.web, size: 24),
|
||||
longPressUrlCopy: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompactPreviewTitle extends StatelessWidget {
|
||||
const _CompactPreviewTitle({required this.tabState});
|
||||
|
||||
final TabState tabState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CompactAppBarTitleView(
|
||||
tabState: tabState,
|
||||
isTabTunneled: false,
|
||||
showSiteSettingsBadge: false,
|
||||
onSiteSettingsTap: _noop,
|
||||
onTitleTap: _noop,
|
||||
tabIcon: const Icon(MdiIcons.web, size: 24),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _noop() {}
|
||||
Reference in New Issue
Block a user