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,
|
||||
data: (categories) {
|
||||
return FadingScroll(
|
||||
@@ -78,7 +79,9 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
||||
.map(
|
||||
(category) => ExpansionPanel(
|
||||
canTapOnHeader: true,
|
||||
isExpanded: expanded.value.contains(category.key),
|
||||
isExpanded: expanded.value.contains(
|
||||
category.key,
|
||||
),
|
||||
headerBuilder: (context, isExpanded) =>
|
||||
ListTile(title: Text(category.key)),
|
||||
body: Padding(
|
||||
@@ -118,6 +121,7 @@ class BangCategoriesScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ class BangMenuScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Bangs')),
|
||||
body: ListView(
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
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,
|
||||
data: (bangs) => FadingScroll(
|
||||
fadingSize: 25,
|
||||
@@ -101,6 +102,7 @@ class BangSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
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> {
|
||||
void build();
|
||||
|
||||
+3
-1
@@ -99,7 +99,8 @@ class BookmarkFolderEditScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Padding(
|
||||
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_design_icons/flutter_material_design_icons.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/domain/providers.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/browser_page.dart';
|
||||
|
||||
class BrowserHome extends ConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -80,79 +68,9 @@ class BrowserHome extends ConsumerWidget {
|
||||
.resumeLatestContainerTab(containerId);
|
||||
}
|
||||
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
bottomViewportInset: bottomViewportInset,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -160,7 +78,7 @@ class BrowserHome extends ConsumerWidget {
|
||||
if (containerData != null)
|
||||
_ContainerHeader(container: containerData)
|
||||
else
|
||||
_BrandHeader(colorScheme: colorScheme),
|
||||
BrandHeader(colorScheme: colorScheme),
|
||||
const SizedBox(height: 24),
|
||||
if (!hasTabs) ...[
|
||||
Text(
|
||||
@@ -207,13 +125,11 @@ class BrowserHome extends ConsumerWidget {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainer.withValues(
|
||||
alpha: 0.9,
|
||||
),
|
||||
color: colorScheme.surfaceContainer.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(
|
||||
color: Color.alphaBlend(
|
||||
_brandGrey.withValues(alpha: 0.18),
|
||||
BrowserPage.brandGrey.withValues(alpha: 0.18),
|
||||
colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
@@ -227,22 +143,16 @@ class BrowserHome extends ConsumerWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
color: theme.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
MdiIcons.formatQuoteOpen,
|
||||
size: 18,
|
||||
),
|
||||
child: const Icon(MdiIcons.formatQuoteOpen, size: 18),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'A thought for the road',
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -258,9 +168,7 @@ class BrowserHome extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
switch (quoteAsync) {
|
||||
AsyncData(:final value) => _QuoteBlock(
|
||||
quote: value,
|
||||
),
|
||||
AsyncData(:final value) => _QuoteBlock(quote: value),
|
||||
AsyncError() => Text(
|
||||
'Open a new tab and make this space your own.',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
@@ -268,9 +176,7 @@ class BrowserHome extends ConsumerWidget {
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
_ => const LinearProgressIndicator(
|
||||
minHeight: 3,
|
||||
),
|
||||
_ => const LinearProgressIndicator(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(
|
||||
appBar: AppBar(title: const Text('Containers')),
|
||||
body: Skeletonizer(
|
||||
body: SafeArea(
|
||||
child: Skeletonizer(
|
||||
enabled: containersAsync.isLoading,
|
||||
child: containersAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
@@ -72,7 +73,9 @@ class ContainerListScreen extends HookConsumerWidget {
|
||||
result ==
|
||||
SetContainerResult.successHasProxy) {
|
||||
final shouldStartProxy = await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.read(
|
||||
startProxyControllerProvider.notifier,
|
||||
)
|
||||
.shouldPromptProxyStart();
|
||||
|
||||
if (!context.mounted || !shouldStartProxy) {
|
||||
@@ -173,6 +176,7 @@ class ContainerListScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final newContainer = await ref
|
||||
|
||||
+7
-2
@@ -45,7 +45,8 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Select Container')),
|
||||
body: Skeletonizer(
|
||||
body: SafeArea(
|
||||
child: Skeletonizer(
|
||||
enabled: containersAsync.isLoading,
|
||||
child: containersAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
@@ -106,13 +107,17 @@ class ContainerSelectionScreen extends HookConsumerWidget {
|
||||
loading: () => ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => ContainerListTile(
|
||||
ContainerData(id: Namespace.nil.value, color: Colors.transparent),
|
||||
ContainerData(
|
||||
id: Namespace.nil.value,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
isSelected: false,
|
||||
onTap: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
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
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/features/geckoview/domain/entities/tab_container_selection.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/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/ai_configuration.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/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/welcome.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
|
||||
@@ -50,6 +58,12 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
|
||||
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>>(() {
|
||||
switch (currentRevision) {
|
||||
@@ -57,25 +71,45 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
return [const AiConfigurationPage()];
|
||||
default:
|
||||
return [
|
||||
const WelcomePage(),
|
||||
WelcomePage(isReturningUser: isReturningUser),
|
||||
const DefaultSearchPage(),
|
||||
if (showDetailedPages) const DohSettingsPage(),
|
||||
if (showDetailedPages) const ToolbarLayoutPage(),
|
||||
const PrivacyHardeningPage(),
|
||||
const AiConfigurationPage(),
|
||||
if (showDetailedPages)
|
||||
UBlockOptInPage(formKey: GlobalKey<FormState>()),
|
||||
PermissionsPage(formKey: GlobalKey<FormState>()),
|
||||
];
|
||||
}
|
||||
}, [currentRevision, targetRevision]);
|
||||
}, [currentRevision, targetRevision, onboardingMode]);
|
||||
|
||||
final lastPage = useRef(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(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
physics: currentPage.value == 0 && !eulaAccepted
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: pages,
|
||||
onPageChanged: (value) {
|
||||
if (value > lastPage.value) {
|
||||
@@ -116,8 +150,9 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
controller: pageController,
|
||||
count: pages.length,
|
||||
effect: WormEffect(
|
||||
dotColor: theme.colorScheme.onSurface,
|
||||
activeDotColor: theme.colorScheme.primary,
|
||||
dotHeight: 10.0,
|
||||
dotWidth: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -125,9 +160,13 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
if (currentPage.value < pages.length - 1)
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
onPressed: () async {
|
||||
onPressed: currentPage.value == 0 && !eulaAccepted
|
||||
? null
|
||||
: () async {
|
||||
if (disableAnimations) {
|
||||
pageController.jumpToPage(currentPage.value + 1);
|
||||
pageController.jumpToPage(
|
||||
currentPage.value + 1,
|
||||
);
|
||||
} else {
|
||||
await pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -149,6 +188,14 @@ class OnboardingScreen extends HookConsumerWidget {
|
||||
formPage.formKey.currentState?.save();
|
||||
}
|
||||
|
||||
if (onboardingMode == OnboardingMode.express) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(browserAddonServiceProvider.notifier)
|
||||
.install('uBlock0@raymondhill.net'),
|
||||
);
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(onboardingRepositoryProvider.notifier)
|
||||
.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/user/data/models/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 {
|
||||
const AiConfigurationPage({super.key});
|
||||
@@ -37,7 +38,10 @@ class AiConfigurationPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
return ListView(
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
@@ -45,6 +49,7 @@ class AiConfigurationPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('On Device AI'),
|
||||
subtitle: const Text(
|
||||
'Local on-device features including container topic and tab suggestions',
|
||||
@@ -63,7 +68,7 @@ class AiConfigurationPage extends HookConsumerWidget {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
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/user/data/models/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/url_icon.dart';
|
||||
|
||||
@@ -61,12 +62,13 @@ class DefaultSearchPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: bangs.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (availableBangs) {
|
||||
return ListView(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
@@ -172,6 +174,7 @@ class DefaultSearchPage extends HookConsumerWidget {
|
||||
),
|
||||
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/>.
|
||||
*/
|
||||
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:permission_handler/permission_handler.dart';
|
||||
import 'package:skeletonizer/skeletonizer.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/widgets/browser_page.dart';
|
||||
|
||||
class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
||||
@override
|
||||
@@ -38,30 +41,38 @@ class PermissionsPage extends HookConsumerWidget implements IFormPage {
|
||||
() => Permission.notification.isGranted,
|
||||
);
|
||||
|
||||
return Form(
|
||||
final isDefaultBrowser = useCachedFuture(
|
||||
() => GeckoBrowserService().isDefaultBrowser(),
|
||||
);
|
||||
|
||||
final isCurrentDefaultBrowser = isDefaultBrowser.data == true;
|
||||
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: Text('Permissions', style: theme.textTheme.headlineMedium),
|
||||
child: Text(
|
||||
'Permissions',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
// child: const Text('WebLibre requires following permissions'),
|
||||
// ),
|
||||
// const SizedBox(height: 24),
|
||||
Skeletonizer(
|
||||
enabled: !notificationPermissionEnabled.hasData,
|
||||
child: FormField(
|
||||
initialValue: true,
|
||||
onSaved: (newValue) async {
|
||||
if (newValue == true) {
|
||||
if (newValue == true &&
|
||||
notificationPermissionEnabled.data != true) {
|
||||
await Permission.notification.request();
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: field.value ?? true,
|
||||
title: const Text('Notifications'),
|
||||
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: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/presentation/widgets/browser_page.dart';
|
||||
|
||||
class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||
@override
|
||||
@@ -36,9 +37,11 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Form(
|
||||
return BrowserPage(
|
||||
child: BrowserPageContent(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
SvgPicture.asset('assets/images/ublock.svg'),
|
||||
@@ -47,9 +50,7 @@ class UBlockOptInPage extends HookConsumerWidget implements IFormPage {
|
||||
child: Text('uBlock Origin', style: theme.textTheme.headlineMedium),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: MarkdownBody(
|
||||
const MarkdownBody(
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
''',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FormField(
|
||||
initialValue: true,
|
||||
@@ -72,6 +72,7 @@ There are many other lists available to block even more.
|
||||
}
|
||||
},
|
||||
builder: (field) => SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: field.value ?? false,
|
||||
title: const Text('Install uBlock Origin Extension'),
|
||||
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/>.
|
||||
*/
|
||||
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 {
|
||||
const WelcomePage({super.key});
|
||||
class WelcomePage extends ConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: SvgPicture.asset('assets/icon/icon.svg'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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(
|
||||
'The Privacy-Focused Browser',
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
'A lot has changed!',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Bang Settings')),
|
||||
body: FadingScroll(
|
||||
body: SafeArea(
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
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/>.
|
||||
*/
|
||||
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';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/doh_settings_content.dart';
|
||||
|
||||
class DohSettingsScreen extends HookConsumerWidget {
|
||||
const DohSettingsScreen({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 Scaffold(
|
||||
appBar: AppBar(title: const Text('DNS over HTTPS')),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const SafeArea(
|
||||
child: SingleChildScrollView(child: DohSettingsContent()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,15 @@
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.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: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/user/data/models/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 {
|
||||
const GeneralSettingsScreen({super.key});
|
||||
@@ -40,7 +44,11 @@ class GeneralSettingsScreen extends StatelessWidget {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
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 {
|
||||
const _AppearanceSection();
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Settings')),
|
||||
body: FadingScroll(
|
||||
body: SafeArea(
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
@@ -50,6 +51,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,22 +19,9 @@
|
||||
*/
|
||||
import 'package:fading_scroll/fading_scroll.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:weblibre/core/routing/routes.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/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/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';
|
||||
|
||||
class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
||||
@@ -55,11 +42,14 @@ class ToolbarLayoutSettingsScreen extends HookConsumerWidget {
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _TabBarPreviewHeaderDelegate(settings: settings),
|
||||
delegate: TabBarPreviewHeaderDelegate(
|
||||
settings: settings,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
const SliverPadding(
|
||||
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) {
|
||||
if (exceptions.isEmpty) {
|
||||
return const _EmptyState();
|
||||
@@ -89,6 +90,7 @@ class TrackingProtectionExceptionsScreen extends HookConsumerWidget {
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
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,
|
||||
data: (data) {
|
||||
return Column(
|
||||
@@ -183,6 +184,7 @@ class WebEngineHardeningScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(groupName)),
|
||||
body: settings.when(
|
||||
body: SafeArea(
|
||||
child: settings.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (group) {
|
||||
return Column(
|
||||
@@ -106,14 +107,17 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
value: setting.value.isActive,
|
||||
title: Text(setting.value.title ?? setting.key),
|
||||
title: Text(
|
||||
setting.value.title ?? setting.key,
|
||||
),
|
||||
subtitle: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (setting.value.requireUserOptIn)
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
@@ -156,9 +160,13 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (value) {
|
||||
await notifier.apply(filter: [setting.key]);
|
||||
await notifier.apply(
|
||||
filter: [setting.key],
|
||||
);
|
||||
} else {
|
||||
await notifier.reset(filter: [setting.key]);
|
||||
await notifier.reset(
|
||||
filter: [setting.key],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -166,8 +174,11 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
||||
else
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
title: Text(setting.value.title ?? setting.key),
|
||||
subtitle: setting.value.description.mapNotNull(
|
||||
title: Text(
|
||||
setting.value.title ?? setting.key,
|
||||
),
|
||||
subtitle: setting.value.description
|
||||
.mapNotNull(
|
||||
(description) => Text(description),
|
||||
),
|
||||
leading: HardeningGroupIcon(
|
||||
@@ -200,6 +211,7 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget {
|
||||
),
|
||||
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,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
@@ -132,6 +133,7 @@ class CountryPickerScreen extends HookWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ class GeneralSettings with FastEquatable {
|
||||
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
|
||||
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true,
|
||||
tabBarPosition = tabBarPosition ?? TabBarPosition.bottom,
|
||||
tabBarLayout = tabBarLayout ?? TabBarLayout.withTitle,
|
||||
tabBarLayout = tabBarLayout ?? TabBarLayout.compact,
|
||||
quickTabSwitcherMode =
|
||||
quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs,
|
||||
pullToRefreshEnabled = pullToRefreshEnabled ?? true,
|
||||
|
||||
@@ -66,7 +66,8 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Create Backup')),
|
||||
body: Padding(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
@@ -145,7 +146,10 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
||||
|
||||
if (confirmation != passwordTextController.text) {
|
||||
if (context.mounted) {
|
||||
showErrorMessage(context, 'Passwords do not match');
|
||||
showErrorMessage(
|
||||
context,
|
||||
'Passwords do not match',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -167,6 +171,7 @@ class ProfileBackupScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,16 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Backups')),
|
||||
body: backupListAsync.when(
|
||||
body: SafeArea(
|
||||
child: backupListAsync.when(
|
||||
data: (backupList) {
|
||||
return ListView.builder(
|
||||
itemCount: backupList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final file = backupList[index];
|
||||
final match = _filenamePattern.firstMatch(p.basename(file.path));
|
||||
final match = _filenamePattern.firstMatch(
|
||||
p.basename(file.path),
|
||||
);
|
||||
|
||||
if (match != null) {
|
||||
final profileName = match.group(1)!;
|
||||
@@ -91,6 +94,7 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,8 @@ class ProfileEditScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
@@ -145,8 +146,11 @@ class ProfileEditScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
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,
|
||||
data: (profiles) => ListView.builder(
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
final isSelected = filesystem.selectedProfile == profile.uuidValue;
|
||||
final isSelected =
|
||||
filesystem.selectedProfile == profile.uuidValue;
|
||||
|
||||
return ListTile(
|
||||
enabled: !isSelected,
|
||||
@@ -76,6 +78,7 @@ class ProfileListScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await CreateProfileRoute().push(context);
|
||||
|
||||
@@ -68,7 +68,8 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Restore Backup')),
|
||||
body: Padding(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
@@ -87,7 +88,10 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
validator: (value) {
|
||||
return validateRequired(value, message: 'Password required');
|
||||
return validateRequired(
|
||||
value,
|
||||
message: 'Password required',
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -172,6 +176,7 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ class LockScreen extends HookConsumerWidget {
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -76,6 +77,7 @@ class LockScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ part 'onboarding.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class OnboardingRepository extends _$OnboardingRepository {
|
||||
static const targetRevision = 2;
|
||||
static const targetRevision = 3;
|
||||
|
||||
Future<int?> getCurrentRevision() {
|
||||
return ref
|
||||
@@ -43,7 +43,7 @@ class OnboardingRepository extends _$OnboardingRepository {
|
||||
|
||||
Future<bool> isOutdated() async {
|
||||
final current = await getCurrentRevision();
|
||||
return current != targetRevision;
|
||||
return current == null || current < targetRevision;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -42,7 +42,7 @@ final class OnboardingRepositoryProvider
|
||||
}
|
||||
|
||||
String _$onboardingRepositoryHash() =>
|
||||
r'937a5fe08c09cc0475270d3707bfb6f087bcf123';
|
||||
r'd16657963415392840cdba9f4bcc724963f6da63';
|
||||
|
||||
abstract class _$OnboardingRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -66,7 +66,8 @@ class FeedListScreen extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: feeds.when(
|
||||
body: SafeArea(
|
||||
child: feeds.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (feeds) {
|
||||
return RefreshIndicator(
|
||||
@@ -95,6 +96,7 @@ class FeedListScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
label: const Text('Feed'),
|
||||
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/landing/
|
||||
- assets/preferences/
|
||||
- assets/legal/
|
||||
- assets/quotes/
|
||||
fonts:
|
||||
- family: TorIcons
|
||||
|
||||
+30
@@ -115,6 +115,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
companion object {
|
||||
private const val TAG = "GeckoBrowserApiImpl"
|
||||
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
|
||||
private const val REQUEST_CODE_BROWSER_ROLE = 1
|
||||
|
||||
private var isGeckoInitialized = false
|
||||
}
|
||||
@@ -424,4 +425,33 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
|
||||
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 onTrimMemory(level: Long)
|
||||
fun openInCustomTab(url: String, private: Boolean, contextId: String?)
|
||||
fun isDefaultBrowser(): Boolean
|
||||
fun requestDefaultBrowser()
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoBrowserApi. */
|
||||
@@ -4947,6 +4949,37 @@ interface GeckoBrowserApi {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> isDefaultBrowser() {
|
||||
return _api.isDefaultBrowser();
|
||||
}
|
||||
|
||||
Future<void> requestDefaultBrowser() {
|
||||
return _api.requestDefaultBrowser();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5756,6 +5756,55 @@ class GeckoBrowserApi {
|
||||
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 {
|
||||
|
||||
@@ -1149,6 +1149,8 @@ abstract class GeckoBrowserApi {
|
||||
required bool private,
|
||||
required String? contextId,
|
||||
});
|
||||
bool isDefaultBrowser();
|
||||
void requestDefaultBrowser();
|
||||
}
|
||||
|
||||
enum SyncEngineValue { history, bookmarks, tabs }
|
||||
|
||||
Reference in New Issue
Block a user