Merge branch 'main' into tor_proxy

This commit is contained in:
Fabian Freund
2025-09-11 15:07:02 +02:00
35 changed files with 495 additions and 163 deletions
@@ -0,0 +1,99 @@
import 'package:drift/drift.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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
import 'package:weblibre/utils/form_validators.dart';
class OpenSharedContent extends HookConsumerWidget {
final Uri sharedUrl;
const OpenSharedContent({required this.sharedUrl});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final textController = useTextEditingController(text: sharedUrl.toString());
final selectedContainer = useState<ContainerData?>(null);
Future<void> openTab(bool isPrivate) async {
if (formKey.currentState?.validate() == true) {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: Uri.parse(textController.text),
private: isPrivate,
container: Value(selectedContainer.value),
launchedFromIntent: true,
);
}
}
return Form(
key: formKey,
child: SimpleDialog(
title: const Text('Open URL'),
children: [
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0),
child: SizedBox(
height: 48,
width: double.maxFinite,
child: ContainerChips(
displayMenu: false,
selectedContainer: selectedContainer.value,
onSelected: (container) {
selectedContainer.value = container;
},
onDeleted: (container) {
selectedContainer.value = null;
},
),
),
),
Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0),
child: TextFormField(
controller: textController,
keyboardType: TextInputType.url,
minLines: 1,
maxLines: 10,
validator: (value) {
return validateUrl(
value,
onlyHttpProtocol: true,
eagerParsing: false,
);
},
),
),
ListTile(
title: const Text('Open Regular Tab'),
leading: const Icon(MdiIcons.tab),
onTap: () async {
await openTab(false);
if (context.mounted) {
context.pop();
}
},
),
ListTile(
title: const Text('Open Private Tab'),
leading: const Icon(MdiIcons.tabUnselected),
onTap: () async {
await openTab(true);
if (context.mounted) {
context.pop();
}
},
),
],
),
);
}
}
@@ -17,6 +17,7 @@
* 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:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
@@ -242,6 +243,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
.addTab(
url: ref.read(docsUriProvider),
private: isPrivate,
container: const Value(null),
);
},
leadingIcon: const Icon(Icons.help),
@@ -301,6 +303,7 @@ class BrowserBottomAppBar extends HookConsumerWidget {
.addTab(
url: Uri.parse('https://addons.mozilla.org'),
private: isPrivate,
container: const Value(null),
);
},
leadingIcon: const Icon(MdiIcons.puzzlePlus),
@@ -86,6 +86,8 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final scrolledTo = useRef(0.0);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!context.mounted) return;
final header = headerKey.currentContext?.findRenderObject();
final text = textFieldKey.currentContext?.findRenderObject();
@@ -96,14 +98,16 @@ class ViewTabSheetWidget extends HookConsumerWidget {
final relative = totalHeight / MediaQuery.of(context).size.height;
if (draggableScrollableController.size < relative &&
relative > scrolledTo.value) {
await draggableScrollableController.animateTo(
relative,
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
);
scrolledTo.value = relative;
if (relative >= 0 && relative <= 1) {
if (draggableScrollableController.size < relative &&
relative > scrolledTo.value) {
await draggableScrollableController.animateTo(
relative,
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
);
scrolledTo.value = relative;
}
}
}
}
@@ -113,22 +117,28 @@ class ViewTabSheetWidget extends HookConsumerWidget {
});
final bottomInsets = useRef(0.0);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final diff =
((MediaQuery.of(context).viewInsets.bottom / 2) /
MediaQuery.of(context).size.height) -
bottomInsets.value;
useEffect(
() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final diff =
((MediaQuery.of(context).viewInsets.bottom / 2) /
MediaQuery.of(context).size.height) -
bottomInsets.value;
draggableScrollableController.jumpTo(
draggableScrollableController.size + diff,
);
draggableScrollableController.jumpTo(
draggableScrollableController.size + diff,
);
bottomInsets.value += diff;
});
bottomInsets.value += diff;
});
return null;
}, [MediaQuery.of(context).viewInsets.bottom]);
return null;
},
[
MediaQuery.of(context).viewInsets.bottom,
MediaQuery.of(context).size.height,
],
);
return NestedScrollView(
physics: const NeverScrollableScrollPhysics(),
@@ -33,6 +33,10 @@ class PreferenceSettingGroup with FastEquatable {
(setting) => setting.requireUserOptIn || setting.isActive,
);
bool get showMasterSwitch =>
settings.values.length >
settings.values.where((setting) => setting.requireUserOptIn).length;
bool get isPartlyActive => settings.values.any((setting) => setting.isActive);
bool get hasInactiveOptional => settings.values.any(
@@ -44,8 +44,13 @@ import 'package:weblibre/utils/uri_parser.dart' as uri_parser;
class SearchScreen extends HookConsumerWidget {
final String? initialSearchText;
final TabType tabType;
final bool launchedFromIntent;
const SearchScreen({required this.initialSearchText, required this.tabType});
const SearchScreen({
required this.initialSearchText,
required this.tabType,
this.launchedFromIntent = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -109,6 +114,7 @@ class SearchScreen extends HookConsumerWidget {
parentId: (selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
);
if (context.mounted) {
@@ -212,6 +218,7 @@ class SearchScreen extends HookConsumerWidget {
(selectedTabType.value == TabType.child)
? ref.read(selectedTabProvider)
: null,
launchedFromIntent: launchedFromIntent,
);
if (context.mounted) {
@@ -1256,28 +1256,28 @@ abstract class _$TabDatabase extends GeneratedDatabase {
);
}
Selectable<String> previousTabByTimestamp({required String tabId}) {
Selectable<String?> previousTabByTimestamp({required String tabId}) {
return customSelect(
'WITH ranked_tabs AS (SELECT id, timestamp, LAG(id)OVER (ORDER BY timestamp RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1',
variables: [Variable<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('prev_tab_id'));
).map((QueryRow row) => row.readNullable<String>('prev_tab_id'));
}
Selectable<String> previousTabByOrderKey({required String tabId}) {
Selectable<String?> previousTabByOrderKey({required String tabId}) {
return customSelect(
'WITH ranked_tabs AS (SELECT id, order_key, LAG(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1',
variables: [Variable<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('prev_tab_id'));
).map((QueryRow row) => row.readNullable<String>('prev_tab_id'));
}
Selectable<String> nextTabByOrderKey({required String tabId}) {
Selectable<String?> nextTabByOrderKey({required String tabId}) {
return customSelect(
'WITH ranked_tabs AS (SELECT id, order_key, LEAD(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_tab_id FROM tab) SELECT next_tab_id FROM ranked_tabs WHERE id = ?1',
variables: [Variable<String>(tabId)],
readsFrom: {tab},
).map((QueryRow row) => row.read<String>('next_tab_id'));
).map((QueryRow row) => row.readNullable<String>('next_tab_id'));
}
@override