update onboarding; add missing safe areas
This commit is contained in:
@@ -53,7 +53,8 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: categoriesAsync.when(
|
body: SafeArea(
|
||||||
|
child: categoriesAsync.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (categories) {
|
data: (categories) {
|
||||||
return FadingScroll(
|
return FadingScroll(
|
||||||
@@ -78,7 +79,9 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
|||||||
.map(
|
.map(
|
||||||
(category) => ExpansionPanel(
|
(category) => ExpansionPanel(
|
||||||
canTapOnHeader: true,
|
canTapOnHeader: true,
|
||||||
isExpanded: expanded.value.contains(category.key),
|
isExpanded: expanded.value.contains(
|
||||||
|
category.key,
|
||||||
|
),
|
||||||
headerBuilder: (context, isExpanded) =>
|
headerBuilder: (context, isExpanded) =>
|
||||||
ListTile(title: Text(category.key)),
|
ListTile(title: Text(category.key)),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
@@ -118,6 +121,7 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class BangMenuScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Bangs')),
|
appBar: AppBar(title: const Text('Bangs')),
|
||||||
body: ListView(
|
body: SafeArea(
|
||||||
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(MdiIcons.accountAlert),
|
leading: const Icon(MdiIcons.accountAlert),
|
||||||
@@ -57,6 +58,7 @@ class BangMenuScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ class BangSearchScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: resultsAsync.when(
|
body: SafeArea(
|
||||||
|
child: resultsAsync.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (bangs) => FadingScroll(
|
data: (bangs) => FadingScroll(
|
||||||
fadingSize: 25,
|
fadingSize: 25,
|
||||||
@@ -101,6 +102,7 @@ class BangSearchScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'87d2a4a0bca93c2d925aa1c81ff80d443185950a';
|
String _$tabRepositoryHash() => r'ce4ac2f2efbb859ba66e98c6e6d21f8205d9b4ed';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+3
-1
@@ -99,7 +99,8 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Form(
|
body: SafeArea(
|
||||||
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
@@ -165,6 +166,7 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-181
@@ -1,6 +1,3 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
@@ -15,20 +12,11 @@ import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors
|
|||||||
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
|
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
|
||||||
import 'package:weblibre/features/quotes/domain/providers.dart';
|
import 'package:weblibre/features/quotes/domain/providers.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class BrowserHome extends ConsumerWidget {
|
class BrowserHome extends ConsumerWidget {
|
||||||
const BrowserHome({super.key});
|
const BrowserHome({super.key});
|
||||||
|
|
||||||
static const _brandPurple = Color(0xFF9C83F8);
|
|
||||||
static const _brandYellow = Color(0xFFFBDC6B);
|
|
||||||
static const _brandGrey = Color(0xFFA7A7A7);
|
|
||||||
|
|
||||||
static const _auraPurple = Color(0xFF2C2543);
|
|
||||||
static const _auraGold = Color(0xFF3C3827);
|
|
||||||
static const _auraShadow = Color(0xFF222224);
|
|
||||||
static const _auraShadowHighlight = Color(0xFF2D2D31);
|
|
||||||
static const _auraTint = Color(0xFF000000);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -80,79 +68,9 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
.resumeLatestContainerTab(containerId);
|
.resumeLatestContainerTab(containerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DecoratedBox(
|
return BrowserPage(
|
||||||
decoration: BoxDecoration(
|
child: BrowserPageContent(
|
||||||
gradient: LinearGradient(
|
bottomViewportInset: bottomViewportInset,
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
Color.alphaBlend(
|
|
||||||
_auraPurple.withValues(alpha: 0.38),
|
|
||||||
colorScheme.surfaceContainerLowest,
|
|
||||||
),
|
|
||||||
Color.alphaBlend(
|
|
||||||
_auraShadow.withValues(alpha: 0.72),
|
|
||||||
colorScheme.surface,
|
|
||||||
),
|
|
||||||
Color.alphaBlend(
|
|
||||||
_auraGold.withValues(alpha: 0.34),
|
|
||||||
colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
const Positioned(
|
|
||||||
top: -70,
|
|
||||||
left: -120,
|
|
||||||
child: _BackdropOrb(width: 400, height: 400, color: _auraPurple),
|
|
||||||
),
|
|
||||||
const Positioned(
|
|
||||||
top: 220,
|
|
||||||
right: -150,
|
|
||||||
child: _BackdropOrb(width: 340, height: 340, color: _auraGold),
|
|
||||||
),
|
|
||||||
const Positioned(
|
|
||||||
bottom: 18,
|
|
||||||
left: -8,
|
|
||||||
child: _BackdropOrb(
|
|
||||||
width: 320,
|
|
||||||
height: 320,
|
|
||||||
color: _auraShadowHighlight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: ClipRect(
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 72, sigmaY: 72),
|
|
||||||
child: ColoredBox(color: _auraTint.withValues(alpha: 0.12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
24,
|
|
||||||
32,
|
|
||||||
24,
|
|
||||||
32 + bottomViewportInset,
|
|
||||||
),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: math.max(
|
|
||||||
0,
|
|
||||||
constraints.maxHeight - 64 - bottomViewportInset,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -160,7 +78,7 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
if (containerData != null)
|
if (containerData != null)
|
||||||
_ContainerHeader(container: containerData)
|
_ContainerHeader(container: containerData)
|
||||||
else
|
else
|
||||||
_BrandHeader(colorScheme: colorScheme),
|
BrandHeader(colorScheme: colorScheme),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (!hasTabs) ...[
|
if (!hasTabs) ...[
|
||||||
Text(
|
Text(
|
||||||
@@ -207,13 +125,11 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.surfaceContainer.withValues(
|
color: colorScheme.surfaceContainer.withValues(alpha: 0.9),
|
||||||
alpha: 0.9,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Color.alphaBlend(
|
color: Color.alphaBlend(
|
||||||
_brandGrey.withValues(alpha: 0.18),
|
BrowserPage.brandGrey.withValues(alpha: 0.18),
|
||||||
colorScheme.outlineVariant,
|
colorScheme.outlineVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -227,22 +143,16 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(MdiIcons.formatQuoteOpen, size: 18),
|
||||||
MdiIcons.formatQuoteOpen,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'A thought for the road',
|
'A thought for the road',
|
||||||
style: theme.textTheme.titleMedium
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -258,9 +168,7 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
switch (quoteAsync) {
|
switch (quoteAsync) {
|
||||||
AsyncData(:final value) => _QuoteBlock(
|
AsyncData(:final value) => _QuoteBlock(quote: value),
|
||||||
quote: value,
|
|
||||||
),
|
|
||||||
AsyncError() => Text(
|
AsyncError() => Text(
|
||||||
'Open a new tab and make this space your own.',
|
'Open a new tab and make this space your own.',
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
@@ -268,9 +176,7 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => const LinearProgressIndicator(
|
_ => const LinearProgressIndicator(minHeight: 3),
|
||||||
minHeight: 3,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -309,58 +215,6 @@ class BrowserHome extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BrandHeader extends StatelessWidget {
|
|
||||||
final ColorScheme colorScheme;
|
|
||||||
|
|
||||||
const _BrandHeader({required this.colorScheme});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
width: 112,
|
|
||||||
height: 112,
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [
|
|
||||||
Color.alphaBlend(
|
|
||||||
BrowserHome._brandPurple.withValues(alpha: 0.18),
|
|
||||||
colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
Color.alphaBlend(
|
|
||||||
BrowserHome._brandYellow.withValues(alpha: 0.12),
|
|
||||||
colorScheme.surfaceContainer,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: colorScheme.outlineVariant.withValues(alpha: 0.45),
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: colorScheme.shadow.withValues(alpha: 0.08),
|
|
||||||
blurRadius: 32,
|
|
||||||
offset: const Offset(0, 18),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: SvgPicture.asset('assets/icon/icon.svg', width: 72, height: 72),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,26 +321,3 @@ class _QuoteBlock extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackdropOrb extends StatelessWidget {
|
|
||||||
final double width;
|
|
||||||
final double height;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const _BackdropOrb({
|
|
||||||
required this.width,
|
|
||||||
required this.height,
|
|
||||||
required this.color,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IgnorePointer(
|
|
||||||
child: Container(
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ class ContainerListScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Containers')),
|
appBar: AppBar(title: const Text('Containers')),
|
||||||
body: Skeletonizer(
|
body: SafeArea(
|
||||||
|
child: Skeletonizer(
|
||||||
enabled: containersAsync.isLoading,
|
enabled: containersAsync.isLoading,
|
||||||
child: containersAsync.when(
|
child: containersAsync.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
@@ -72,7 +73,9 @@ class ContainerListScreen extends HookConsumerWidget {
|
|||||||
result ==
|
result ==
|
||||||
SetContainerResult.successHasProxy) {
|
SetContainerResult.successHasProxy) {
|
||||||
final shouldStartProxy = await ref
|
final shouldStartProxy = await ref
|
||||||
.read(startProxyControllerProvider.notifier)
|
.read(
|
||||||
|
startProxyControllerProvider.notifier,
|
||||||
|
)
|
||||||
.shouldPromptProxyStart();
|
.shouldPromptProxyStart();
|
||||||
|
|
||||||
if (!context.mounted || !shouldStartProxy) {
|
if (!context.mounted || !shouldStartProxy) {
|
||||||
@@ -173,6 +176,7 @@ class ContainerListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final newContainer = await ref
|
final newContainer = await ref
|
||||||
|
|||||||
+7
-2
@@ -45,7 +45,8 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Select Container')),
|
appBar: AppBar(title: const Text('Select Container')),
|
||||||
body: Skeletonizer(
|
body: SafeArea(
|
||||||
|
child: Skeletonizer(
|
||||||
enabled: containersAsync.isLoading,
|
enabled: containersAsync.isLoading,
|
||||||
child: containersAsync.when(
|
child: containersAsync.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
@@ -106,13 +107,17 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
|||||||
loading: () => ListView.builder(
|
loading: () => ListView.builder(
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
itemBuilder: (context, index) => ContainerListTile(
|
itemBuilder: (context, index) => ContainerListTile(
|
||||||
ContainerData(id: Namespace.nil.value, color: Colors.transparent),
|
ContainerData(
|
||||||
|
id: Namespace.nil.value,
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
onTap: null,
|
onTap: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final newContainer = await ref
|
final newContainer = await ref
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum OnboardingMode {
|
||||||
|
express,
|
||||||
|
detailed,
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||||
|
|
||||||
|
part 'providers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class OnboardingModeNotifier extends _$OnboardingModeNotifier {
|
||||||
|
@override
|
||||||
|
OnboardingMode build() => OnboardingMode.detailed;
|
||||||
|
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void select(OnboardingMode mode) {
|
||||||
|
state = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class EulaAcceptedNotifier extends _$EulaAcceptedNotifier {
|
||||||
|
@override
|
||||||
|
bool build() => false;
|
||||||
|
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void accept(bool value) {
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(OnboardingModeNotifier)
|
||||||
|
final onboardingModeProvider = OnboardingModeNotifierProvider._();
|
||||||
|
|
||||||
|
final class OnboardingModeNotifierProvider
|
||||||
|
extends $NotifierProvider<OnboardingModeNotifier, OnboardingMode> {
|
||||||
|
OnboardingModeNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'onboardingModeProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$onboardingModeNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
OnboardingModeNotifier create() => OnboardingModeNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(OnboardingMode value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<OnboardingMode>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$onboardingModeNotifierHash() =>
|
||||||
|
r'bbb8e7ec1c85699566750d90c92c273e031d654b';
|
||||||
|
|
||||||
|
abstract class _$OnboardingModeNotifier extends $Notifier<OnboardingMode> {
|
||||||
|
OnboardingMode build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<OnboardingMode, OnboardingMode>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<OnboardingMode, OnboardingMode>,
|
||||||
|
OnboardingMode,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProviderFor(EulaAcceptedNotifier)
|
||||||
|
final eulaAcceptedProvider = EulaAcceptedNotifierProvider._();
|
||||||
|
|
||||||
|
final class EulaAcceptedNotifierProvider
|
||||||
|
extends $NotifierProvider<EulaAcceptedNotifier, bool> {
|
||||||
|
EulaAcceptedNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'eulaAcceptedProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$eulaAcceptedNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
EulaAcceptedNotifier create() => EulaAcceptedNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(bool value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<bool>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$eulaAcceptedNotifierHash() =>
|
||||||
|
r'dafd708ac5cb586e8ca9cccc6d708598c5daab0e';
|
||||||
|
|
||||||
|
abstract class _$EulaAcceptedNotifier extends $Notifier<bool> {
|
||||||
|
bool build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<bool, bool>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<bool, bool>,
|
||||||
|
bool,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@@ -25,11 +27,17 @@ import 'package:weblibre/core/providers/defaults.dart';
|
|||||||
import 'package:weblibre/core/providers/router.dart';
|
import 'package:weblibre/core/providers/router.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/domain/providers.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/ai_configuration.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/ai_configuration.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/default_search.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/default_search.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/presentation/pages/doh_settings.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/permissions.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/permissions.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/presentation/pages/privacy_hardening.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/presentation/pages/toolbar_layout.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/welcome.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/welcome.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||||
@@ -50,6 +58,12 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||||
|
|
||||||
final pageController = usePageController();
|
final pageController = usePageController();
|
||||||
|
final eulaAccepted = ref.watch(eulaAcceptedProvider);
|
||||||
|
final onboardingMode = ref.watch(onboardingModeProvider);
|
||||||
|
|
||||||
|
final isReturningUser = currentRevision == 2;
|
||||||
|
final showDetailedPages =
|
||||||
|
isReturningUser || onboardingMode == OnboardingMode.detailed;
|
||||||
|
|
||||||
final pages = useMemoized<List<Widget>>(() {
|
final pages = useMemoized<List<Widget>>(() {
|
||||||
switch (currentRevision) {
|
switch (currentRevision) {
|
||||||
@@ -57,25 +71,45 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
return [const AiConfigurationPage()];
|
return [const AiConfigurationPage()];
|
||||||
default:
|
default:
|
||||||
return [
|
return [
|
||||||
const WelcomePage(),
|
WelcomePage(isReturningUser: isReturningUser),
|
||||||
const DefaultSearchPage(),
|
const DefaultSearchPage(),
|
||||||
|
if (showDetailedPages) const DohSettingsPage(),
|
||||||
|
if (showDetailedPages) const ToolbarLayoutPage(),
|
||||||
|
const PrivacyHardeningPage(),
|
||||||
const AiConfigurationPage(),
|
const AiConfigurationPage(),
|
||||||
|
if (showDetailedPages)
|
||||||
UBlockOptInPage(formKey: GlobalKey<FormState>()),
|
UBlockOptInPage(formKey: GlobalKey<FormState>()),
|
||||||
PermissionsPage(formKey: GlobalKey<FormState>()),
|
PermissionsPage(formKey: GlobalKey<FormState>()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}, [currentRevision, targetRevision]);
|
}, [currentRevision, targetRevision, onboardingMode]);
|
||||||
|
|
||||||
final lastPage = useRef(pageController.initialPage);
|
final lastPage = useRef(pageController.initialPage);
|
||||||
final currentPage = useState(pageController.initialPage);
|
final currentPage = useState(pageController.initialPage);
|
||||||
|
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
|
canPop: currentPage.value == 0,
|
||||||
|
onPopInvokedWithResult: (didPop, _) async {
|
||||||
|
if (didPop) return;
|
||||||
|
if (disableAnimations) {
|
||||||
|
pageController.jumpToPage(currentPage.value - 1);
|
||||||
|
} else {
|
||||||
|
await pageController.previousPage(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView(
|
child: PageView(
|
||||||
controller: pageController,
|
controller: pageController,
|
||||||
|
physics: currentPage.value == 0 && !eulaAccepted
|
||||||
|
? const NeverScrollableScrollPhysics()
|
||||||
|
: null,
|
||||||
children: pages,
|
children: pages,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value) {
|
||||||
if (value > lastPage.value) {
|
if (value > lastPage.value) {
|
||||||
@@ -116,8 +150,9 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
controller: pageController,
|
controller: pageController,
|
||||||
count: pages.length,
|
count: pages.length,
|
||||||
effect: WormEffect(
|
effect: WormEffect(
|
||||||
dotColor: theme.colorScheme.onSurface,
|
|
||||||
activeDotColor: theme.colorScheme.primary,
|
activeDotColor: theme.colorScheme.primary,
|
||||||
|
dotHeight: 10.0,
|
||||||
|
dotWidth: 10.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -125,9 +160,13 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
if (currentPage.value < pages.length - 1)
|
if (currentPage.value < pages.length - 1)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () async {
|
onPressed: currentPage.value == 0 && !eulaAccepted
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
if (disableAnimations) {
|
if (disableAnimations) {
|
||||||
pageController.jumpToPage(currentPage.value + 1);
|
pageController.jumpToPage(
|
||||||
|
currentPage.value + 1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await pageController.nextPage(
|
await pageController.nextPage(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
@@ -149,6 +188,14 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
formPage.formKey.currentState?.save();
|
formPage.formKey.currentState?.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onboardingMode == OnboardingMode.express) {
|
||||||
|
unawaited(
|
||||||
|
ref
|
||||||
|
.read(browserAddonServiceProvider.notifier)
|
||||||
|
.install('uBlock0@raymondhill.net'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(onboardingRepositoryProvider.notifier)
|
.read(onboardingRepositoryProvider.notifier)
|
||||||
.pushRevision(targetRevision);
|
.pushRevision(targetRevision);
|
||||||
@@ -175,6 +222,7 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class AiConfigurationPage extends HookConsumerWidget {
|
class AiConfigurationPage extends HookConsumerWidget {
|
||||||
const AiConfigurationPage({super.key});
|
const AiConfigurationPage({super.key});
|
||||||
@@ -37,7 +38,10 @@ class AiConfigurationPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListView(
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Center(
|
Center(
|
||||||
@@ -45,6 +49,7 @@ class AiConfigurationPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SwitchListTile.adaptive(
|
SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
title: const Text('On Device AI'),
|
title: const Text('On Device AI'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Local on-device features including container topic and tab suggestions',
|
'Local on-device features including container topic and tab suggestions',
|
||||||
@@ -63,7 +68,7 @@ class AiConfigurationPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
@@ -102,6 +107,8 @@ class AiConfigurationPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import 'package:weblibre/features/settings/presentation/controllers/save_setting
|
|||||||
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
|
|
||||||
@@ -61,12 +62,13 @@ class DefaultSearchPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return BrowserPage(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
child: BrowserPageContent(
|
||||||
child: bangs.when(
|
child: bangs.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (availableBangs) {
|
data: (availableBangs) {
|
||||||
return ListView(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Center(
|
Center(
|
||||||
@@ -172,6 +174,7 @@ class DefaultSearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const SizedBox(height: 48, width: double.infinity),
|
loading: () => const SizedBox(height: 48, width: double.infinity),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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/settings/presentation/widgets/doh_settings_content.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
|
class DohSettingsPage extends HookConsumerWidget {
|
||||||
|
const DohSettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'DNS over HTTPS',
|
||||||
|
style: theme.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const ListTileTheme(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
child: DohSettingsContent(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,11 +18,14 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
||||||
|
show GeckoBrowserService;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
||||||
@override
|
@override
|
||||||
@@ -38,30 +41,38 @@ class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
|||||||
() => Permission.notification.isGranted,
|
() => Permission.notification.isGranted,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Form(
|
final isDefaultBrowser = useCachedFuture(
|
||||||
|
() => GeckoBrowserService().isDefaultBrowser(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isCurrentDefaultBrowser = isDefaultBrowser.data == true;
|
||||||
|
|
||||||
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: ListView(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Center(
|
Center(
|
||||||
child: Text('Permissions', style: theme.textTheme.headlineMedium),
|
child: Text(
|
||||||
|
'Permissions',
|
||||||
|
style: theme.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Padding(
|
|
||||||
// padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
// child: const Text('WebLibre requires following permissions'),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
Skeletonizer(
|
Skeletonizer(
|
||||||
enabled: !notificationPermissionEnabled.hasData,
|
enabled: !notificationPermissionEnabled.hasData,
|
||||||
child: FormField(
|
child: FormField(
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
onSaved: (newValue) async {
|
onSaved: (newValue) async {
|
||||||
if (newValue == true) {
|
if (newValue == true &&
|
||||||
|
notificationPermissionEnabled.data != true) {
|
||||||
await Permission.notification.request();
|
await Permission.notification.request();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (field) => SwitchListTile(
|
builder: (field) => SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
value: field.value ?? true,
|
value: field.value ?? true,
|
||||||
title: const Text('Notifications'),
|
title: const Text('Notifications'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
@@ -75,8 +86,35 @@ class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Skeletonizer(
|
||||||
|
enabled: !isDefaultBrowser.hasData,
|
||||||
|
child: FormField(
|
||||||
|
key: ValueKey(isDefaultBrowser.data),
|
||||||
|
initialValue: isCurrentDefaultBrowser,
|
||||||
|
onSaved: (newValue) async {
|
||||||
|
if (newValue == true && isDefaultBrowser.data == false) {
|
||||||
|
await GeckoBrowserService().requestDefaultBrowser();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (field) => SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: field.value ?? isCurrentDefaultBrowser,
|
||||||
|
title: const Text('Default Browser'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Set WebLibre as your default browser',
|
||||||
|
),
|
||||||
|
onChanged: isCurrentDefaultBrowser
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
field.didChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
/*
|
||||||
|
* 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/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/utils/setting_groups_serializer.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/entities/fingerprint_overrides.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
|
class PrivacyHardeningPage extends ConsumerWidget {
|
||||||
|
const PrivacyHardeningPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Center(
|
||||||
|
child: Text(
|
||||||
|
'Privacy & Hardening',
|
||||||
|
style: theme.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const _LanguageFingerprintWarning(),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Browser Languages'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Configure language preferences exposed to websites',
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.translate),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
await LocaleSettingsRoute().push(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const _WebEngineHardeningToggle(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const _FingerprintProtectionToggle(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const _LocalNetworkAccessSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LanguageFingerprintWarning extends ConsumerWidget {
|
||||||
|
const _LanguageFingerprintWarning();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final configuredLocales = ref.watch(
|
||||||
|
engineSettingsWithDefaultsProvider.select((s) => s.locales),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (configuredLocales.length <= 1) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
margin: const EdgeInsets.only(bottom: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.errorContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onErrorContainer,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'Multiple Languages Detected\n\n',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onErrorContainer,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '• '),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
'Your browser has ${configuredLocales.length} languages '
|
||||||
|
'configured (${configuredLocales.join(', ')}). '
|
||||||
|
'Websites can use your unique language combination to '
|
||||||
|
'fingerprint and track you across the web.\n\n',
|
||||||
|
),
|
||||||
|
const TextSpan(text: '• '),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
'Consider reducing your browser languages to a single '
|
||||||
|
'language to minimize your fingerprint surface.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.onErrorContainer,
|
||||||
|
foregroundColor: theme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
await LocaleSettingsRoute().push(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.translate),
|
||||||
|
label: const Text('Review Languages'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebEngineHardeningToggle extends ConsumerWidget {
|
||||||
|
const _WebEngineHardeningToggle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final preferenceGroups = ref.watch(
|
||||||
|
unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user),
|
||||||
|
);
|
||||||
|
|
||||||
|
final allGroupsActive = preferenceGroups.maybeWhen(
|
||||||
|
data: (data) =>
|
||||||
|
data.values.every((element) => element.isActiveOrOptional),
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Complete Web Engine Hardening'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Apply all recommended security hardening preferences to the web engine',
|
||||||
|
),
|
||||||
|
secondary: const Icon(MdiIcons.shieldLock),
|
||||||
|
value: allGroupsActive,
|
||||||
|
onChanged: preferenceGroups.hasValue
|
||||||
|
? (value) async {
|
||||||
|
final notifier = ref.read(
|
||||||
|
unifiedPreferenceSettingsRepositoryProvider(
|
||||||
|
PreferencePartition.user,
|
||||||
|
).notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
await notifier.apply();
|
||||||
|
} else {
|
||||||
|
await notifier.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FingerprintProtectionToggle extends ConsumerWidget {
|
||||||
|
const _FingerprintProtectionToggle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final currentOverrides = ref.watch(
|
||||||
|
engineSettingsWithDefaultsProvider.select(
|
||||||
|
(s) => s.fingerprintingProtectionOverrides,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final isHardened =
|
||||||
|
currentOverrides == FingerprintOverrides.hardenedDefaults().toString();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Hardened Fingerprint Protection'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Load comprehensive fingerprint protection defaults',
|
||||||
|
),
|
||||||
|
secondary: const Icon(MdiIcons.fingerprint),
|
||||||
|
value: isHardened,
|
||||||
|
onChanged: (value) async {
|
||||||
|
final overrides = value
|
||||||
|
? FingerprintOverrides.hardenedDefaults()
|
||||||
|
: FingerprintOverrides.defaults();
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(saveEngineSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) => currentSettings.copyWith
|
||||||
|
.fingerprintingProtectionOverrides(overrides.toString()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: isHardened
|
||||||
|
? Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
margin: const EdgeInsets.only(top: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onError,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: 'Compatibility Warning\n\n',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onError,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '• '),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
'Hardened fingerprint protection enables 60+ '
|
||||||
|
'protection targets including canvas '
|
||||||
|
'randomization, navigator spoofing, media device '
|
||||||
|
'masking, and more.\n\n',
|
||||||
|
),
|
||||||
|
const TextSpan(text: '• '),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
'This may cause websites to break or behave '
|
||||||
|
'unexpectedly. You can fine-tune individual '
|
||||||
|
'targets in settings.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LocalNetworkAccessSection extends ConsumerWidget {
|
||||||
|
const _LocalNetworkAccessSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final lnaBlocking = ref.watch(
|
||||||
|
engineSettingsWithDefaultsProvider.select((s) => s.lnaBlocking),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const ListTile(
|
||||||
|
title: Text('Local Network Protection'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Websites can try to reach your device and other devices '
|
||||||
|
'on your home network, like routers, printers, or smart home '
|
||||||
|
'devices. By default, known trackers are automatically blocked '
|
||||||
|
'from doing this.',
|
||||||
|
),
|
||||||
|
leading: Icon(MdiIcons.lanDisconnect),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Block All Local Network Requests'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Ask for permission before any website accesses devices '
|
||||||
|
'on your home network, not just known trackers',
|
||||||
|
),
|
||||||
|
secondary: const Icon(MdiIcons.shieldLockOpen),
|
||||||
|
value: lnaBlocking ?? false,
|
||||||
|
onChanged: (value) async {
|
||||||
|
await ref
|
||||||
|
.read(saveEngineSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) =>
|
||||||
|
currentSettings.copyWith.lnaBlocking(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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/settings/presentation/widgets/toolbar_layout_content.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/widgets/toolbar_preview.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
|
class ToolbarLayoutPage extends HookConsumerWidget {
|
||||||
|
const ToolbarLayoutPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
return BrowserPage(
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 32, 12, 16),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Toolbar & Layout',
|
||||||
|
style: theme.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPersistentHeader(
|
||||||
|
pinned: true,
|
||||||
|
delegate: TabBarPreviewHeaderDelegate(
|
||||||
|
settings: settings,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
compact: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 32),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: const ListTileTheme(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
child: ToolbarLayoutContent(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
import 'package:weblibre/features/onboarding/presentation/pages/abstract/i_form_page.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||||
@override
|
@override
|
||||||
@@ -36,9 +37,11 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Form(
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: ListView(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
SvgPicture.asset('assets/images/ublock.svg'),
|
SvgPicture.asset('assets/images/ublock.svg'),
|
||||||
@@ -47,9 +50,7 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
|||||||
child: Text('uBlock Origin', style: theme.textTheme.headlineMedium),
|
child: Text('uBlock Origin', style: theme.textTheme.headlineMedium),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Padding(
|
const MarkdownBody(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: MarkdownBody(
|
|
||||||
data: '''
|
data: '''
|
||||||
uBlock Origin (uBO) is a CPU and memory-efficient **wide-spectrum content blocker** by **Raymond Hill** and available as a browser extensions for WebLibre.
|
uBlock Origin (uBO) is a CPU and memory-efficient **wide-spectrum content blocker** by **Raymond Hill** and available as a browser extensions for WebLibre.
|
||||||
|
|
||||||
@@ -58,7 +59,6 @@ It blocks ads, trackers, coin miners, popups, annoying anti-blockers, malware si
|
|||||||
There are many other lists available to block even more.
|
There are many other lists available to block even more.
|
||||||
''',
|
''',
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
FormField(
|
FormField(
|
||||||
initialValue: true,
|
initialValue: true,
|
||||||
@@ -72,6 +72,7 @@ There are many other lists available to block even more.
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (field) => SwitchListTile(
|
builder: (field) => SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
value: field.value ?? false,
|
value: field.value ?? false,
|
||||||
title: const Text('Install uBlock Origin Extension'),
|
title: const Text('Install uBlock Origin Extension'),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -81,6 +82,8 @@ There are many other lists available to block even more.
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,28 +18,390 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/domain/entities/onboarding_mode.dart';
|
||||||
|
import 'package:weblibre/features/onboarding/domain/providers.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||||
|
|
||||||
class WelcomePage extends StatelessWidget {
|
class WelcomePage extends ConsumerWidget {
|
||||||
const WelcomePage({super.key});
|
final bool isReturningUser;
|
||||||
|
|
||||||
|
const WelcomePage({super.key, this.isReturningUser = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
final selectedMode = ref.watch(onboardingModeProvider);
|
||||||
|
final accepted = ref.watch(eulaAcceptedProvider);
|
||||||
|
|
||||||
|
return BrowserPage(
|
||||||
|
child: BrowserPageContent(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
BrandHeader(colorScheme: colorScheme),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
isReturningUser ? 'Welcome back!' : 'WebLibre is ready',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isReturningUser) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_UpdateNotice(colorScheme: colorScheme, theme: theme),
|
||||||
|
],
|
||||||
|
if (!isReturningUser) ...[
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'Choose your onboarding experience:',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_ModeOption(
|
||||||
|
mode: OnboardingMode.express,
|
||||||
|
title: 'Quick Start',
|
||||||
|
subtitle: 'Use recommended defaults and get browsing.',
|
||||||
|
icon: Icons.bolt,
|
||||||
|
selected: selectedMode == OnboardingMode.express,
|
||||||
|
onTap: () => ref
|
||||||
|
.read(onboardingModeProvider.notifier)
|
||||||
|
.select(OnboardingMode.express),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_ModeOption(
|
||||||
|
mode: OnboardingMode.detailed,
|
||||||
|
title: 'Custom Setup',
|
||||||
|
subtitle: 'Configure DNS, toolbar, extensions, and more.',
|
||||||
|
icon: Icons.tune,
|
||||||
|
selected: selectedMode == OnboardingMode.detailed,
|
||||||
|
onTap: () => ref
|
||||||
|
.read(onboardingModeProvider.notifier)
|
||||||
|
.select(OnboardingMode.detailed),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
_EulaCheckbox(
|
||||||
|
accepted: accepted,
|
||||||
|
onAcceptedChanged: (value) =>
|
||||||
|
ref.read(eulaAcceptedProvider.notifier).accept(value),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpdateNotice extends StatelessWidget {
|
||||||
|
final ColorScheme colorScheme;
|
||||||
|
final ThemeData theme;
|
||||||
|
|
||||||
|
const _UpdateNotice({required this.colorScheme, required this.theme});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 8),
|
||||||
FractionallySizedBox(
|
Container(
|
||||||
widthFactor: 0.5,
|
padding: const EdgeInsets.all(16),
|
||||||
child: SvgPicture.asset('assets/icon/icon.svg'),
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.auto_awesome,
|
||||||
|
color: BrowserPage.brandYellow,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'The Privacy-Focused Browser',
|
'A lot has changed!',
|
||||||
softWrap: true,
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.w600,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'This update includes significant changes that require '
|
||||||
|
'you to review your settings. Please walk through the '
|
||||||
|
'following pages to revalidate your configuration.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.extension,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Please re-check your extensions after this update '
|
||||||
|
'due to known migration issues.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.shield_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Your existing settings will not be overridden '
|
||||||
|
'unless you explicitly change them during this setup.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ModeOption extends StatelessWidget {
|
||||||
|
final OnboardingMode mode;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final IconData icon;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ModeOption({
|
||||||
|
required this.mode,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.icon,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: selected
|
||||||
|
? colorScheme.primaryContainer.withValues(alpha: 0.4)
|
||||||
|
: colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: selected ? colorScheme.primary : Colors.transparent,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: selected
|
||||||
|
? BrowserPage.brandYellow
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: selected
|
||||||
|
? colorScheme.onSurface
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: selected
|
||||||
|
? colorScheme.onSurfaceVariant
|
||||||
|
: colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (selected) Icon(Icons.check_circle, color: colorScheme.primary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EulaCheckbox extends StatelessWidget {
|
||||||
|
final bool accepted;
|
||||||
|
final ValueChanged<bool> onAcceptedChanged;
|
||||||
|
|
||||||
|
const _EulaCheckbox({
|
||||||
|
required this.accepted,
|
||||||
|
required this.onAcceptedChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return CheckboxListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: accepted,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
onChanged: (value) => onAcceptedChanged(value ?? false),
|
||||||
|
title: Wrap(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'I have read and accept the ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showLegalDocument(
|
||||||
|
context,
|
||||||
|
title: 'End User License Agreement',
|
||||||
|
assetPath: 'assets/legal/EULA.md',
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'EULA',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: BrowserPage.brandYellow,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: BrowserPage.brandYellow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' and ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showLegalDocument(
|
||||||
|
context,
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
assetPath: 'assets/legal/PRIVACY_POLICY.md',
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Privacy Policy',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: BrowserPage.brandYellow,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: BrowserPage.brandYellow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'.',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showLegalDocument(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String assetPath,
|
||||||
|
}) async {
|
||||||
|
final content = await rootBundle.loadString(assetPath);
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (context) => DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.9,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
maxChildSize: 0.95,
|
||||||
|
expand: false,
|
||||||
|
builder: (context, scrollController) => Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: Markdown(controller: scrollController, data: content),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class BangSettingsScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Bang Settings')),
|
appBar: AppBar(title: const Text('Bang Settings')),
|
||||||
body: FadingScroll(
|
body: SafeArea(
|
||||||
|
child: FadingScroll(
|
||||||
fadingSize: 25,
|
fadingSize: 25,
|
||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return ListView(
|
return ListView(
|
||||||
@@ -72,6 +73,7 @@ class BangSettingsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,155 +18,18 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/doh_settings_content.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 DohSettingsScreen extends HookConsumerWidget {
|
class DohSettingsScreen extends HookConsumerWidget {
|
||||||
const DohSettingsScreen({super.key});
|
const DohSettingsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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 Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('DNS over HTTPS')),
|
appBar: AppBar(title: const Text('DNS over HTTPS')),
|
||||||
body: SafeArea(
|
body: const SafeArea(
|
||||||
child: ListView(
|
child: SingleChildScrollView(child: DohSettingsContent()),
|
||||||
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’re 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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,15 @@
|
|||||||
import 'package:fading_scroll/fading_scroll.dart';
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
||||||
|
show GeckoBrowserService;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/widgets/sections.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/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
|
||||||
class GeneralSettingsScreen extends StatelessWidget {
|
class GeneralSettingsScreen extends StatelessWidget {
|
||||||
const GeneralSettingsScreen({super.key});
|
const GeneralSettingsScreen({super.key});
|
||||||
@@ -40,7 +44,11 @@ class GeneralSettingsScreen extends StatelessWidget {
|
|||||||
return ListView(
|
return ListView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
children: const [_AppearanceSection(), _DownloadsSection()],
|
children: const [
|
||||||
|
_DefaultBrowserSection(),
|
||||||
|
_AppearanceSection(),
|
||||||
|
_DownloadsSection(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -49,6 +57,67 @@ class GeneralSettingsScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DefaultBrowserSection extends StatelessWidget {
|
||||||
|
const _DefaultBrowserSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Column(
|
||||||
|
children: [
|
||||||
|
SettingSection(name: 'Default Browser'),
|
||||||
|
_DefaultBrowserTile(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultBrowserTile extends HookConsumerWidget {
|
||||||
|
const _DefaultBrowserTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final defaultBrowserRefreshKey = useState(0);
|
||||||
|
|
||||||
|
useOnAppLifecycleStateChange((previous, current) {
|
||||||
|
if (current == AppLifecycleState.resumed) {
|
||||||
|
defaultBrowserRefreshKey.value++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final isDefault = useCachedFuture(
|
||||||
|
() => GeckoBrowserService().isDefaultBrowser(),
|
||||||
|
[defaultBrowserRefreshKey.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
final isCurrentDefaultBrowser = isDefault.data == true;
|
||||||
|
|
||||||
|
return CustomListTile(
|
||||||
|
title: 'Default Browser',
|
||||||
|
subtitle: isCurrentDefaultBrowser
|
||||||
|
? 'WebLibre is your default browser'
|
||||||
|
: 'Set WebLibre as your default browser',
|
||||||
|
prefix: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.public,
|
||||||
|
size: 24,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffix: FilledButton.icon(
|
||||||
|
onPressed: isCurrentDefaultBrowser
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await GeckoBrowserService().requestDefaultBrowser();
|
||||||
|
defaultBrowserRefreshKey.value++;
|
||||||
|
},
|
||||||
|
icon: Icon(isCurrentDefaultBrowser ? Icons.check : Icons.open_in_new),
|
||||||
|
label: Text(isCurrentDefaultBrowser ? 'Default' : 'Set'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AppearanceSection extends StatelessWidget {
|
class _AppearanceSection extends StatelessWidget {
|
||||||
const _AppearanceSection();
|
const _AppearanceSection();
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Settings')),
|
appBar: AppBar(title: const Text('Settings')),
|
||||||
body: FadingScroll(
|
body: SafeArea(
|
||||||
|
child: FadingScroll(
|
||||||
fadingSize: 25,
|
fadingSize: 25,
|
||||||
builder: (context, controller) {
|
builder: (context, controller) {
|
||||||
return ListView(
|
return ListView(
|
||||||
@@ -50,6 +51,7 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,22 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
import 'package:fading_scroll/fading_scroll.dart';
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/toolbar_layout_content.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/toolbar_preview.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/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';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
||||||
@@ -55,11 +42,14 @@ class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverPersistentHeader(
|
||||||
pinned: true,
|
pinned: true,
|
||||||
delegate: _TabBarPreviewHeaderDelegate(settings: settings),
|
delegate: TabBarPreviewHeaderDelegate(
|
||||||
|
settings: settings,
|
||||||
|
compact: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SliverPadding(
|
const SliverPadding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
sliver: SliverToBoxAdapter(child: _ToolbarLayoutContent()),
|
sliver: SliverToBoxAdapter(child: ToolbarLayoutContent()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -69,744 +59,3 @@ class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ToolbarLayoutContent extends StatelessWidget {
|
|
||||||
const _ToolbarLayoutContent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Column(
|
|
||||||
children: [
|
|
||||||
SettingSection(name: 'Tab Bar'),
|
|
||||||
_TabBarPositionSection(),
|
|
||||||
_TabBarLayoutModeSection(),
|
|
||||||
_AutoHideTabBarTile(),
|
|
||||||
SettingSection(name: 'Contextual Toolbar'),
|
|
||||||
_ShowContextualTabBarTile(),
|
|
||||||
_CustomizeToolbarButtonsTile(),
|
|
||||||
SettingSection(name: 'Quick Tab Switcher'),
|
|
||||||
_ShowQuickTabSwitcherBarTile(),
|
|
||||||
_QuickTabSwitcherModeSection(),
|
|
||||||
_QuickTabSwitcherHistorySuggestionsTile(),
|
|
||||||
_QuickTabSwitcherShowTitlesTile(),
|
|
||||||
SettingSection(name: 'Tab View'),
|
|
||||||
_BottomSheetTabViewTile(),
|
|
||||||
_TabListShowFaviconsTile(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabBarPreviewHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|
||||||
const _TabBarPreviewHeaderDelegate({required this.settings});
|
|
||||||
|
|
||||||
static const _kPreviewBaseHeight = 164.0;
|
|
||||||
|
|
||||||
final GeneralSettings settings;
|
|
||||||
|
|
||||||
double get _toolbarHeight {
|
|
||||||
var height = kToolbarHeight;
|
|
||||||
|
|
||||||
if (settings.tabBarShowContextualBar) {
|
|
||||||
height += BrowserTabBar.contextualToolabarHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.tabBarShowQuickTabSwitcherBar) {
|
|
||||||
height += BrowserTabBar.quickTabSwitcherHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get minExtent => _kPreviewBaseHeight + _toolbarHeight;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get maxExtent => _kPreviewBaseHeight + _toolbarHeight;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(
|
|
||||||
BuildContext context,
|
|
||||||
double shrinkOffset,
|
|
||||||
bool overlapsContent,
|
|
||||||
) {
|
|
||||||
return ColoredBox(
|
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
||||||
child: _TabBarPreviewCard(settings: settings),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRebuild(covariant _TabBarPreviewHeaderDelegate oldDelegate) {
|
|
||||||
return oldDelegate.settings != settings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabBarPreviewCard extends HookWidget {
|
|
||||||
const _TabBarPreviewCard({required this.settings});
|
|
||||||
|
|
||||||
final GeneralSettings settings;
|
|
||||||
|
|
||||||
@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(),
|
|
||||||
);
|
|
||||||
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colorScheme.surface,
|
|
||||||
border: Border.all(color: colorScheme.outlineVariant),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (settings.tabBarPosition == TabBarPosition.top)
|
|
||||||
topMainToolbar,
|
|
||||||
Container(
|
|
||||||
height: 72,
|
|
||||||
width: double.infinity,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: 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,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {}
|
|
||||||
|
|
||||||
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 _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 _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 _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 _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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ class TrackingProtectionExceptionsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: exceptionsAsync.when(
|
body: SafeArea(
|
||||||
|
child: exceptionsAsync.when(
|
||||||
data: (exceptions) {
|
data: (exceptions) {
|
||||||
if (exceptions.isEmpty) {
|
if (exceptions.isEmpty) {
|
||||||
return const _EmptyState();
|
return const _EmptyState();
|
||||||
@@ -89,6 +90,7 @@ class TrackingProtectionExceptionsScreen extends HookConsumerWidget {
|
|||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, stack) => _ErrorState(error: error.toString()),
|
error: (error, stack) => _ErrorState(error: error.toString()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ class WebEngineHardeningScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: preferenceGroups.when(
|
body: SafeArea(
|
||||||
|
child: preferenceGroups.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (data) {
|
data: (data) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -183,6 +184,7 @@ class WebEngineHardeningScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(groupName)),
|
appBar: AppBar(title: Text(groupName)),
|
||||||
body: settings.when(
|
body: SafeArea(
|
||||||
|
child: settings.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (group) {
|
data: (group) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -106,14 +107,17 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
value: setting.value.isActive,
|
value: setting.value.isActive,
|
||||||
title: Text(setting.value.title ?? setting.key),
|
title: Text(
|
||||||
|
setting.value.title ?? setting.key,
|
||||||
|
),
|
||||||
subtitle: Text.rich(
|
subtitle: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
if (setting.value.requireUserOptIn)
|
if (setting.value.requireUserOptIn)
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
horizontal: 6,
|
horizontal: 6,
|
||||||
vertical: 2,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
@@ -156,9 +160,13 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
await notifier.apply(filter: [setting.key]);
|
await notifier.apply(
|
||||||
|
filter: [setting.key],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await notifier.reset(filter: [setting.key]);
|
await notifier.reset(
|
||||||
|
filter: [setting.key],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -166,8 +174,11 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
|||||||
else
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(setting.value.title ?? setting.key),
|
title: Text(
|
||||||
subtitle: setting.value.description.mapNotNull(
|
setting.value.title ?? setting.key,
|
||||||
|
),
|
||||||
|
subtitle: setting.value.description
|
||||||
|
.mapNotNull(
|
||||||
(description) => Text(description),
|
(description) => Text(description),
|
||||||
),
|
),
|
||||||
leading: HardeningGroupIcon(
|
leading: HardeningGroupIcon(
|
||||||
@@ -200,6 +211,7 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,452 @@
|
|||||||
|
/*
|
||||||
|
* 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(),
|
||||||
|
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 _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,380 @@
|
|||||||
|
/*
|
||||||
|
* 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {}
|
||||||
@@ -99,7 +99,8 @@ class CountryPickerScreen extends HookWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
body: SafeArea(
|
||||||
|
child: ListView.builder(
|
||||||
itemCount: filteredCountries.length + 1,
|
itemCount: filteredCountries.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
@@ -132,6 +133,7 @@ class CountryPickerScreen extends HookWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ class GeneralSettings with FastEquatable {
|
|||||||
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
|
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
|
||||||
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true,
|
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true,
|
||||||
tabBarPosition = tabBarPosition ?? TabBarPosition.bottom,
|
tabBarPosition = tabBarPosition ?? TabBarPosition.bottom,
|
||||||
tabBarLayout = tabBarLayout ?? TabBarLayout.withTitle,
|
tabBarLayout = tabBarLayout ?? TabBarLayout.compact,
|
||||||
quickTabSwitcherMode =
|
quickTabSwitcherMode =
|
||||||
quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs,
|
quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs,
|
||||||
pullToRefreshEnabled = pullToRefreshEnabled ?? true,
|
pullToRefreshEnabled = pullToRefreshEnabled ?? true,
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Create Backup')),
|
appBar: AppBar(title: const Text('Create Backup')),
|
||||||
body: Padding(
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
@@ -145,7 +146,10 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (confirmation != passwordTextController.text) {
|
if (confirmation != passwordTextController.text) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showErrorMessage(context, 'Passwords do not match');
|
showErrorMessage(
|
||||||
|
context,
|
||||||
|
'Passwords do not match',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -167,6 +171,7 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,13 +39,16 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Backups')),
|
appBar: AppBar(title: const Text('Backups')),
|
||||||
body: backupListAsync.when(
|
body: SafeArea(
|
||||||
|
child: backupListAsync.when(
|
||||||
data: (backupList) {
|
data: (backupList) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: backupList.length,
|
itemCount: backupList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final file = backupList[index];
|
final file = backupList[index];
|
||||||
final match = _filenamePattern.firstMatch(p.basename(file.path));
|
final match = _filenamePattern.firstMatch(
|
||||||
|
p.basename(file.path),
|
||||||
|
);
|
||||||
|
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
final profileName = match.group(1)!;
|
final profileName = match.group(1)!;
|
||||||
@@ -91,6 +94,7 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ class ProfileEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Form(
|
body: SafeArea(
|
||||||
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
@@ -145,8 +146,11 @@ class ProfileEditScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (profile != null) ...[_ProfileActionsSection(profile: profile!)],
|
if (profile != null) ...[
|
||||||
|
_ProfileActionsSection(profile: profile!),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,13 +46,15 @@ class ProfileListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: usersAsync.when(
|
body: SafeArea(
|
||||||
|
child: usersAsync.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (profiles) => ListView.builder(
|
data: (profiles) => ListView.builder(
|
||||||
itemCount: profiles.length,
|
itemCount: profiles.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final profile = profiles[index];
|
final profile = profiles[index];
|
||||||
final isSelected = filesystem.selectedProfile == profile.uuidValue;
|
final isSelected =
|
||||||
|
filesystem.selectedProfile == profile.uuidValue;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
enabled: !isSelected,
|
enabled: !isSelected,
|
||||||
@@ -76,6 +78,7 @@ class ProfileListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await CreateProfileRoute().push(context);
|
await CreateProfileRoute().push(context);
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Restore Backup')),
|
appBar: AppBar(title: const Text('Restore Backup')),
|
||||||
body: Padding(
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
@@ -87,7 +88,10 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
return validateRequired(value, message: 'Password required');
|
return validateRequired(
|
||||||
|
value,
|
||||||
|
message: 'Password required',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -172,6 +176,7 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class LockScreen extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -76,6 +77,7 @@ class LockScreen extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ part 'onboarding.g.dart';
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class OnboardingRepository extends _$OnboardingRepository {
|
class OnboardingRepository extends _$OnboardingRepository {
|
||||||
static const targetRevision = 2;
|
static const targetRevision = 3;
|
||||||
|
|
||||||
Future<int?> getCurrentRevision() {
|
Future<int?> getCurrentRevision() {
|
||||||
return ref
|
return ref
|
||||||
@@ -43,7 +43,7 @@ class OnboardingRepository extends _$OnboardingRepository {
|
|||||||
|
|
||||||
Future<bool> isOutdated() async {
|
Future<bool> isOutdated() async {
|
||||||
final current = await getCurrentRevision();
|
final current = await getCurrentRevision();
|
||||||
return current != targetRevision;
|
return current == null || current < targetRevision;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class OnboardingRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$onboardingRepositoryHash() =>
|
String _$onboardingRepositoryHash() =>
|
||||||
r'937a5fe08c09cc0475270d3707bfb6f087bcf123';
|
r'd16657963415392840cdba9f4bcc724963f6da63';
|
||||||
|
|
||||||
abstract class _$OnboardingRepository extends $Notifier<void> {
|
abstract class _$OnboardingRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ class FeedListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: feeds.when(
|
body: SafeArea(
|
||||||
|
child: feeds.when(
|
||||||
skipLoadingOnReload: true,
|
skipLoadingOnReload: true,
|
||||||
data: (feeds) {
|
data: (feeds) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
@@ -95,6 +96,7 @@ class FeedListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
label: const Text('Feed'),
|
label: const Text('Feed'),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class BrowserPage extends ConsumerWidget {
|
||||||
|
final double bottomViewportInset;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const BrowserPage({super.key, this.bottomViewportInset = 0, required this.child});
|
||||||
|
|
||||||
|
static const brandPurple = Color(0xFF9C83F8);
|
||||||
|
static const brandYellow = Color(0xFFFBDC6B);
|
||||||
|
static const brandGrey = Color(0xFFA7A7A7);
|
||||||
|
|
||||||
|
static const auraPurple = Color(0xFF2C2543);
|
||||||
|
static const auraGold = Color(0xFF3C3827);
|
||||||
|
static const auraShadow = Color(0xFF222224);
|
||||||
|
static const auraShadowHighlight = Color(0xFF2D2D31);
|
||||||
|
static const auraTint = Color(0xFF000000);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color.alphaBlend(
|
||||||
|
auraPurple.withValues(alpha: 0.38),
|
||||||
|
colorScheme.surfaceContainerLowest,
|
||||||
|
),
|
||||||
|
Color.alphaBlend(
|
||||||
|
auraShadow.withValues(alpha: 0.72),
|
||||||
|
colorScheme.surface,
|
||||||
|
),
|
||||||
|
Color.alphaBlend(
|
||||||
|
auraGold.withValues(alpha: 0.34),
|
||||||
|
colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
const Positioned(
|
||||||
|
top: -70,
|
||||||
|
left: -120,
|
||||||
|
child: _BackdropOrb(width: 400, height: 400, color: auraPurple),
|
||||||
|
),
|
||||||
|
const Positioned(
|
||||||
|
top: 220,
|
||||||
|
right: -150,
|
||||||
|
child: _BackdropOrb(width: 340, height: 340, color: auraGold),
|
||||||
|
),
|
||||||
|
const Positioned(
|
||||||
|
bottom: 18,
|
||||||
|
left: -8,
|
||||||
|
child: _BackdropOrb(
|
||||||
|
width: 320,
|
||||||
|
height: 320,
|
||||||
|
color: auraShadowHighlight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 72, sigmaY: 72),
|
||||||
|
child: ColoredBox(color: auraTint.withValues(alpha: 0.12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(child: child),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrowserPageContent extends StatelessWidget {
|
||||||
|
final double bottomViewportInset;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const BrowserPageContent({
|
||||||
|
super.key,
|
||||||
|
this.bottomViewportInset = 0,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
24,
|
||||||
|
32,
|
||||||
|
24,
|
||||||
|
32 + bottomViewportInset,
|
||||||
|
),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: math.max(
|
||||||
|
0,
|
||||||
|
constraints.maxHeight - 64 - bottomViewportInset,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrandHeader extends StatelessWidget {
|
||||||
|
final ColorScheme colorScheme;
|
||||||
|
|
||||||
|
const BrandHeader({super.key, required this.colorScheme});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color.alphaBlend(
|
||||||
|
BrowserPage.brandPurple.withValues(alpha: 0.18),
|
||||||
|
colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
Color.alphaBlend(
|
||||||
|
BrowserPage.brandYellow.withValues(alpha: 0.12),
|
||||||
|
colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant.withValues(alpha: 0.45),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.shadow.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 32,
|
||||||
|
offset: const Offset(0, 18),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SvgPicture.asset('assets/icon/icon.svg', width: 72, height: 72),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackdropOrb extends StatelessWidget {
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _BackdropOrb({
|
||||||
|
required this.width,
|
||||||
|
required this.height,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ flutter:
|
|||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/landing/
|
- assets/landing/
|
||||||
- assets/preferences/
|
- assets/preferences/
|
||||||
|
- assets/legal/
|
||||||
- assets/quotes/
|
- assets/quotes/
|
||||||
fonts:
|
fonts:
|
||||||
- family: TorIcons
|
- family: TorIcons
|
||||||
|
|||||||
+30
@@ -115,6 +115,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "GeckoBrowserApiImpl"
|
private const val TAG = "GeckoBrowserApiImpl"
|
||||||
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
|
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
|
||||||
|
private const val REQUEST_CODE_BROWSER_ROLE = 1
|
||||||
|
|
||||||
private var isGeckoInitialized = false
|
private var isGeckoInitialized = false
|
||||||
}
|
}
|
||||||
@@ -424,4 +425,33 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
|
|
||||||
logger.debug("$TAG: Opened custom tab ${tab.id} for $url (private=$`private`)")
|
logger.debug("$TAG: Opened custom tab ${tab.id} for $url (private=$`private`)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isDefaultBrowser(): Boolean {
|
||||||
|
val currentActivity = requireNotNull(activity) { "Activity not attached" }
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
val roleManager = currentActivity.getSystemService(android.app.role.RoleManager::class.java)
|
||||||
|
return roleManager.isRoleHeld(android.app.role.RoleManager.ROLE_BROWSER)
|
||||||
|
}
|
||||||
|
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse("http://"))
|
||||||
|
val resolveInfo = currentActivity.packageManager.resolveActivity(intent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
return resolveInfo?.activityInfo?.packageName == currentActivity.packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestDefaultBrowser() {
|
||||||
|
val currentActivity = requireNotNull(activity) { "Activity not attached" }
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||||
|
val roleManager = currentActivity.getSystemService(android.app.role.RoleManager::class.java)
|
||||||
|
if (roleManager.isRoleAvailable(android.app.role.RoleManager.ROLE_BROWSER) &&
|
||||||
|
!roleManager.isRoleHeld(android.app.role.RoleManager.ROLE_BROWSER)) {
|
||||||
|
currentActivity.startActivityForResult(
|
||||||
|
roleManager.createRequestRoleIntent(android.app.role.RoleManager.ROLE_BROWSER),
|
||||||
|
REQUEST_CODE_BROWSER_ROLE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intent = android.content.Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
|
||||||
|
currentActivity.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -4845,6 +4845,8 @@ interface GeckoBrowserApi {
|
|||||||
fun showNativeFragment(): Boolean
|
fun showNativeFragment(): Boolean
|
||||||
fun onTrimMemory(level: Long)
|
fun onTrimMemory(level: Long)
|
||||||
fun openInCustomTab(url: String, private: Boolean, contextId: String?)
|
fun openInCustomTab(url: String, private: Boolean, contextId: String?)
|
||||||
|
fun isDefaultBrowser(): Boolean
|
||||||
|
fun requestDefaultBrowser()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by GeckoBrowserApi. */
|
/** The codec used by GeckoBrowserApi. */
|
||||||
@@ -4947,6 +4949,37 @@ interface GeckoBrowserApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.isDefaultBrowser$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.isDefaultBrowser())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
GeckoPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.requestDefaultBrowser$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.requestDefaultBrowser()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
GeckoPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,12 @@ class GeckoBrowserService {
|
|||||||
contextId: contextId,
|
contextId: contextId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isDefaultBrowser() {
|
||||||
|
return _api.isDefaultBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestDefaultBrowser() {
|
||||||
|
return _api.requestDefaultBrowser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5756,6 +5756,55 @@ class GeckoBrowserApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isDefaultBrowser() async {
|
||||||
|
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.isDefaultBrowser$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as bool?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestDefaultBrowser() async {
|
||||||
|
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.requestDefaultBrowser$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeckoSyncApi {
|
class GeckoSyncApi {
|
||||||
|
|||||||
@@ -1149,6 +1149,8 @@ abstract class GeckoBrowserApi {
|
|||||||
required bool private,
|
required bool private,
|
||||||
required String? contextId,
|
required String? contextId,
|
||||||
});
|
});
|
||||||
|
bool isDefaultBrowser();
|
||||||
|
void requestDefaultBrowser();
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SyncEngineValue { history, bookmarks, tabs }
|
enum SyncEngineValue { history, bookmarks, tabs }
|
||||||
|
|||||||
Reference in New Issue
Block a user