move improve ui; added url cleaner everywhere
This commit is contained in:
+1
-1
@@ -61,7 +61,7 @@ final class ToolbarVisibilityControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$toolbarVisibilityControllerHash() =>
|
String _$toolbarVisibilityControllerHash() =>
|
||||||
r'242449893afd6780d977544659e83d4641c8a0ad';
|
r'e947508c351d4cbbe8c63289171a6d0a34f1a61a';
|
||||||
|
|
||||||
final class ToolbarVisibilityControllerFamily extends $Family
|
final class ToolbarVisibilityControllerFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+81
-15
@@ -50,6 +50,9 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/dialog
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/url_cleaner_tile.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart';
|
import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
@@ -120,11 +123,6 @@ class _BrowserMenuSheet extends HookConsumerWidget {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
// Navigation row
|
|
||||||
if (selectedTabId != null)
|
|
||||||
_NavigationRow(selectedTabId: selectedTabId),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Quick toggles (Desktop Mode / Reader Mode)
|
// Quick toggles (Desktop Mode / Reader Mode)
|
||||||
if (selectedTabId != null) ...[
|
if (selectedTabId != null) ...[
|
||||||
_QuickTogglesGrid(selectedTabId: selectedTabId),
|
_QuickTogglesGrid(selectedTabId: selectedTabId),
|
||||||
@@ -161,6 +159,18 @@ class _BrowserMenuSheet extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Persistent navigation row at the bottom
|
||||||
|
if (selectedTabId != null) ...[
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom,
|
||||||
|
top: 8,
|
||||||
|
),
|
||||||
|
child: _NavigationRow(selectedTabId: selectedTabId),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -187,12 +197,14 @@ Widget _buildSubTile(
|
|||||||
String title, {
|
String title, {
|
||||||
IconData? icon,
|
IconData? icon,
|
||||||
Color? iconColor,
|
Color? iconColor,
|
||||||
|
Widget? trailing,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(left: 56, right: 16),
|
contentPadding: const EdgeInsets.only(left: 56, right: 16),
|
||||||
leading: icon != null ? Icon(icon, color: iconColor, size: 20) : null,
|
leading: icon != null ? Icon(icon, color: iconColor, size: 20) : null,
|
||||||
title: Text(title, style: const TextStyle(fontSize: 14)),
|
title: Text(title, style: const TextStyle(fontSize: 14)),
|
||||||
|
trailing: trailing,
|
||||||
dense: true,
|
dense: true,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
);
|
||||||
@@ -1013,20 +1025,74 @@ class _ShareExpansion extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
final catalogAsync = ref.watch(urlCleanerCatalogServiceProvider);
|
||||||
|
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||||
|
final tabUrl = tabState?.url;
|
||||||
|
|
||||||
|
final cleanedUrl = useState<Uri?>(null);
|
||||||
|
final cleaner = useUrlCleanerController(
|
||||||
|
sourceUrl: (cleanedUrl.value ?? tabUrl)?.toString(),
|
||||||
|
rules: catalogAsync.value,
|
||||||
|
cleanerEnabled: settings.urlCleanerEnabled,
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
|
autoApply: settings.urlCleanerAutoApply,
|
||||||
|
getCurrentUrl: () => (cleanedUrl.value ?? tabUrl)?.toString(),
|
||||||
|
onApplyCleanedUrl: (cleanedUrlValue) {
|
||||||
|
cleanedUrl.value = Uri.parse(cleanedUrlValue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
void applyCleanUrl() {
|
||||||
|
if (cleaner.applyCleanUrl()) {
|
||||||
|
ui_helper.showInfoMessage(context, 'URL cleaned');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void applySelectedTrackingRemovals(String previewUrl) {
|
||||||
|
if (cleaner.applyPreviewUrl(previewUrl)) {
|
||||||
|
ui_helper.showInfoMessage(context, 'URL preview applied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveUrl = cleanedUrl.value ?? tabUrl;
|
||||||
|
final cleaningHappened = cleanedUrl.value != null;
|
||||||
|
final hasActiveTracking = cleaner.result?.removedParams.isNotEmpty ?? false;
|
||||||
|
final cleanedTrailing = cleaningHappened
|
||||||
|
? Icon(
|
||||||
|
hasActiveTracking
|
||||||
|
? MdiIcons.shieldLinkVariantOutline
|
||||||
|
: MdiIcons.shieldLinkVariant,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final showCleanerTile = tabUrl != null && cleaner.showTile;
|
||||||
|
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
leading: const Icon(Icons.share),
|
leading: const Icon(Icons.share),
|
||||||
title: const Text('Share'),
|
title: const Text('Share'),
|
||||||
children: [
|
children: [
|
||||||
|
if (showCleanerTile)
|
||||||
|
UrlCleanerTile(
|
||||||
|
result: cleaner.result!,
|
||||||
|
currentUrl: effectiveUrl?.toString() ?? '',
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
|
onClean: applyCleanUrl,
|
||||||
|
onApplySelectedRemovals: applySelectedTrackingRemovals,
|
||||||
|
applied: cleaner.applied,
|
||||||
|
),
|
||||||
|
|
||||||
// Copy Address
|
// Copy Address
|
||||||
_buildSubTile(
|
_buildSubTile(
|
||||||
'Copy Address',
|
'Copy Address',
|
||||||
icon: MdiIcons.contentCopy,
|
icon: MdiIcons.contentCopy,
|
||||||
|
trailing: cleanedTrailing,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(text: tabState.url.toString()),
|
ClipboardData(text: effectiveUrl.toString()),
|
||||||
);
|
);
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
@@ -1044,7 +1110,7 @@ class _ShareExpansion extends HookConsumerWidget {
|
|||||||
.read(selectedTabSessionProvider)
|
.read(selectedTabSessionProvider)
|
||||||
.requestScreenshot();
|
.requestScreenshot();
|
||||||
|
|
||||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
final ts = ref.read(tabStateProvider(selectedTabId))!;
|
||||||
|
|
||||||
if (screenshot != null) {
|
if (screenshot != null) {
|
||||||
ui.decodeImageFromList(screenshot, (result) async {
|
ui.decodeImageFromList(screenshot, (result) async {
|
||||||
@@ -1062,7 +1128,7 @@ class _ShareExpansion extends HookConsumerWidget {
|
|||||||
await SharePlus.instance.share(
|
await SharePlus.instance.share(
|
||||||
ShareParams(
|
ShareParams(
|
||||||
files: [file],
|
files: [file],
|
||||||
subject: tabState.titleOrAuthority,
|
subject: ts.titleOrAuthority,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1080,9 +1146,9 @@ class _ShareExpansion extends HookConsumerWidget {
|
|||||||
_buildSubTile(
|
_buildSubTile(
|
||||||
'Share Link',
|
'Share Link',
|
||||||
icon: Icons.share,
|
icon: Icons.share,
|
||||||
|
trailing: cleanedTrailing,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
await SharePlus.instance.share(ShareParams(uri: effectiveUrl));
|
||||||
await SharePlus.instance.share(ShareParams(uri: tabState.url));
|
|
||||||
if (context.mounted) Navigator.pop(context);
|
if (context.mounted) Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1094,11 +1160,11 @@ class _ShareExpansion extends HookConsumerWidget {
|
|||||||
_buildSubTile(
|
_buildSubTile(
|
||||||
'Show QR Code',
|
'Show QR Code',
|
||||||
icon: Icons.qr_code,
|
icon: Icons.qr_code,
|
||||||
|
trailing: cleanedTrailing,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final tabState = ref.read(tabStateProvider(selectedTabId))!;
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await showQrCode(context, tabState.url.toString());
|
await showQrCode(context, effectiveUrl.toString());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1716,8 +1782,8 @@ class _ProfileCard extends HookConsumerWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
final result = await showQuitBrowserDialog(context);
|
final result = await showQuitBrowserDialog(context);
|
||||||
|
|
||||||
if (result == true && context.mounted) {
|
if (result == true) {
|
||||||
await exitApp(ProviderScope.containerOf(context));
|
await exitApp(ref.container);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+11
-28
@@ -42,8 +42,8 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.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/app_bar_title.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/share_bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
||||||
@@ -289,9 +289,6 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
displayedSheet is! ViewTabsSheet)
|
displayedSheet is! ViewTabsSheet)
|
||||||
? ContainerColors.forAppBar(containerColor)
|
? ContainerColors.forAppBar(containerColor)
|
||||||
: null,
|
: null,
|
||||||
leading: showMainToolbarNavigationButton
|
|
||||||
? NavigationMenuButton(selectedTabId: selectedTabId)
|
|
||||||
: null,
|
|
||||||
title:
|
title:
|
||||||
(selectedTabId != null && displayedSheet is! ViewTabsSheet)
|
(selectedTabId != null && displayedSheet is! ViewTabsSheet)
|
||||||
? const AppBarTitle()
|
? const AppBarTitle()
|
||||||
@@ -356,6 +353,8 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
displayedSheet: displayedSheet,
|
displayedSheet: displayedSheet,
|
||||||
showLongPressMenu: true,
|
showLongPressMenu: true,
|
||||||
),
|
),
|
||||||
|
if (showMainToolbarNavigationButton)
|
||||||
|
NavigationMenuButton(selectedTabId: selectedTabId),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -421,7 +420,6 @@ class ContextualToolbar extends HookConsumerWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
NavigationMenuButton(selectedTabId: selectedTabId),
|
|
||||||
if (tabState?.historyState.canGoBack == true ||
|
if (tabState?.historyState.canGoBack == true ||
|
||||||
tabState?.isLoading == true)
|
tabState?.isLoading == true)
|
||||||
NavigateBackButton(
|
NavigateBackButton(
|
||||||
@@ -447,6 +445,7 @@ class ContextualToolbar extends HookConsumerWidget {
|
|||||||
displayedSheet: displayedSheet,
|
displayedSheet: displayedSheet,
|
||||||
showLongPressMenu: false,
|
showLongPressMenu: false,
|
||||||
),
|
),
|
||||||
|
NavigationMenuButton(selectedTabId: selectedTabId),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -642,38 +641,22 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShareMenuButton extends HookConsumerWidget {
|
class ShareMenuButton extends StatelessWidget {
|
||||||
final String? selectedTabId;
|
final String? selectedTabId;
|
||||||
|
|
||||||
const ShareMenuButton({super.key, required this.selectedTabId});
|
const ShareMenuButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final menuController = useMenuController();
|
|
||||||
|
|
||||||
return MenuAnchor(
|
|
||||||
controller: menuController,
|
|
||||||
menuChildren: [
|
|
||||||
CopyAddressMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
ShareMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
SendTabToDeviceMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
|
||||||
],
|
|
||||||
builder: (context, controller, child) {
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (controller.isOpen) {
|
final tabId = selectedTabId;
|
||||||
controller.close();
|
if (tabId != null) {
|
||||||
} else {
|
await showShareBottomSheet(context, selectedTabId: tabId);
|
||||||
controller.open();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,7 +671,7 @@ class NavigationMenuButton extends StatelessWidget {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
await showBrowserMenuSheet(context);
|
await showBrowserMenuSheet(context);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.menu),
|
child: const Icon(Icons.more_vert),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+468
@@ -0,0 +1,468 @@
|
|||||||
|
/*
|
||||||
|
* 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 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.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:share_plus/share_plus.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/url_cleaner_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/tracking_details_dialog.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart';
|
||||||
|
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
Future<void> showShareBottomSheet(
|
||||||
|
BuildContext context, {
|
||||||
|
required String selectedTabId,
|
||||||
|
}) {
|
||||||
|
return showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => ShareBottomSheet(selectedTabId: selectedTabId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShareBottomSheet extends HookConsumerWidget {
|
||||||
|
final String selectedTabId;
|
||||||
|
|
||||||
|
const ShareBottomSheet({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
|
final catalogAsync = ref.watch(urlCleanerCatalogServiceProvider);
|
||||||
|
|
||||||
|
final tabUrl = ref.watch(
|
||||||
|
tabStateProvider(selectedTabId).select((v) => v?.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
final cleanedUrl = useState<Uri?>(null);
|
||||||
|
final cleaner = useUrlCleanerController(
|
||||||
|
sourceUrl: (cleanedUrl.value ?? tabUrl)?.toString(),
|
||||||
|
rules: catalogAsync.value,
|
||||||
|
cleanerEnabled: settings.urlCleanerEnabled,
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
|
autoApply: settings.urlCleanerAutoApply,
|
||||||
|
getCurrentUrl: () => (cleanedUrl.value ?? tabUrl)?.toString(),
|
||||||
|
onApplyCleanedUrl: (cleanedUrlValue) {
|
||||||
|
cleanedUrl.value = Uri.parse(cleanedUrlValue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
void applyCleanUrl() {
|
||||||
|
if (cleaner.applyCleanUrl()) {
|
||||||
|
ui_helper.showInfoMessage(context, 'URL cleaned');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void applySelectedTrackingRemovals(String previewUrl) {
|
||||||
|
if (cleaner.applyPreviewUrl(previewUrl)) {
|
||||||
|
ui_helper.showInfoMessage(context, 'URL preview applied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveUrl = cleanedUrl.value ?? tabUrl;
|
||||||
|
final cleaningHappened = cleanedUrl.value != null;
|
||||||
|
final hasActiveTracking = cleaner.result?.removedParams.isNotEmpty ?? false;
|
||||||
|
final urlWasCleaned = cleaningHappened && !hasActiveTracking;
|
||||||
|
final trackingStatusTrailing = cleaningHappened
|
||||||
|
? Icon(
|
||||||
|
hasActiveTracking
|
||||||
|
? MdiIcons.shieldLinkVariantOutline
|
||||||
|
: MdiIcons.shieldLinkVariant,
|
||||||
|
size: 18,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
final showCleanerTile = tabUrl != null && cleaner.showTile;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Header with URL and tracking status
|
||||||
|
_ShareHeader(
|
||||||
|
url: effectiveUrl?.toString() ?? '',
|
||||||
|
urlWasCleaned: urlWasCleaned,
|
||||||
|
hasTracking: showCleanerTile && !cleaner.applied,
|
||||||
|
cleanerResult: showCleanerTile ? cleaner.result : null,
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
|
onClean: showCleanerTile && !cleaner.applied
|
||||||
|
? applyCleanUrl
|
||||||
|
: null,
|
||||||
|
onApplySelectedRemovals: applySelectedTrackingRemovals,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Copy Address
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(MdiIcons.contentCopy),
|
||||||
|
title: const Text('Copy Address'),
|
||||||
|
trailing: trackingStatusTrailing,
|
||||||
|
onTap: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: effectiveUrl.toString()),
|
||||||
|
);
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Open in App (conditional)
|
||||||
|
_OpenInAppTile(selectedTabId: selectedTabId),
|
||||||
|
|
||||||
|
// Share Screenshot
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.mobile_screen_share),
|
||||||
|
title: const Text('Share Screenshot'),
|
||||||
|
onTap: () async {
|
||||||
|
final screenshot = await ref
|
||||||
|
.read(selectedTabSessionProvider)
|
||||||
|
.requestScreenshot();
|
||||||
|
|
||||||
|
final ts = ref.read(tabStateProvider(selectedTabId))!;
|
||||||
|
|
||||||
|
if (screenshot != null) {
|
||||||
|
ui.decodeImageFromList(screenshot, (result) async {
|
||||||
|
try {
|
||||||
|
final png = await result.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (png != null) {
|
||||||
|
final file = XFile.fromData(
|
||||||
|
png.buffer.asUint8List(),
|
||||||
|
mimeType: 'image/png',
|
||||||
|
);
|
||||||
|
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [file],
|
||||||
|
subject: ts.titleOrAuthority,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
result.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Share Link
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.share),
|
||||||
|
title: const Text('Share Link'),
|
||||||
|
trailing: trackingStatusTrailing,
|
||||||
|
onTap: () async {
|
||||||
|
await SharePlus.instance.share(ShareParams(uri: effectiveUrl));
|
||||||
|
if (context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// Send To Device (conditional)
|
||||||
|
_SendToDeviceTile(selectedTabId: selectedTabId),
|
||||||
|
|
||||||
|
// Show QR Code
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.qr_code),
|
||||||
|
title: const Text('Show QR Code'),
|
||||||
|
trailing: trackingStatusTrailing,
|
||||||
|
onTap: () async {
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
await showQrCode(context, effectiveUrl.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareHeader extends StatelessWidget {
|
||||||
|
final String url;
|
||||||
|
final bool urlWasCleaned;
|
||||||
|
final bool hasTracking;
|
||||||
|
final UrlCleanerResult? cleanerResult;
|
||||||
|
final bool allowReferralMarketing;
|
||||||
|
final VoidCallback? onClean;
|
||||||
|
final ValueChanged<String>? onApplySelectedRemovals;
|
||||||
|
|
||||||
|
const _ShareHeader({
|
||||||
|
required this.url,
|
||||||
|
required this.urlWasCleaned,
|
||||||
|
required this.hasTracking,
|
||||||
|
required this.allowReferralMarketing,
|
||||||
|
this.cleanerResult,
|
||||||
|
this.onClean,
|
||||||
|
this.onApplySelectedRemovals,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final paramCount = cleanerResult?.removedParams.length ?? 0;
|
||||||
|
final hasTappableDetails = paramCount > 0;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: hasTappableDetails
|
||||||
|
? () {
|
||||||
|
unawaited(
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => TrackingDetailsDialog(
|
||||||
|
currentUrl: url,
|
||||||
|
result: cleanerResult!,
|
||||||
|
allowReferralMarketing: allowReferralMarketing,
|
||||||
|
onApplySelectedRemovals: onApplySelectedRemovals,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
url,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: urlWasCleaned
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (hasTracking)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
size: 14,
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
paramCount == 1
|
||||||
|
? '1 tracking parameter detected'
|
||||||
|
: '$paramCount tracking parameters detected',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else if (urlWasCleaned)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
MdiIcons.checkCircle,
|
||||||
|
size: 14,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Link is clean',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasTracking && onClean != null)
|
||||||
|
IconButton.filledTonal(
|
||||||
|
onPressed: onClean,
|
||||||
|
icon: const Icon(MdiIcons.linkVariantRemove),
|
||||||
|
tooltip: 'Remove tracking',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OpenInAppTile extends HookConsumerWidget {
|
||||||
|
final String selectedTabId;
|
||||||
|
|
||||||
|
static final _service = GeckoAppLinksService();
|
||||||
|
|
||||||
|
const _OpenInAppTile({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||||
|
final url = tabState?.url;
|
||||||
|
final hasExternalApp = useCachedFuture(
|
||||||
|
() => url != null ? _service.hasExternalApp(url) : Future.value(false),
|
||||||
|
[url],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasExternalApp.data != true) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.open_in_new),
|
||||||
|
title: const Text('Open in App'),
|
||||||
|
onTap: () async {
|
||||||
|
if (url == null) return;
|
||||||
|
final success = await _service.openAppLink(url);
|
||||||
|
if (success && context.mounted) Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SendToDeviceTile extends ConsumerWidget {
|
||||||
|
final String selectedTabId;
|
||||||
|
|
||||||
|
const _SendToDeviceTile({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||||
|
final devices = ref.watch(syncDevicesProvider);
|
||||||
|
|
||||||
|
if (!isAuthenticated) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: devices.isLoading && devices.value == null,
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||||
|
child: ExpansionTile(
|
||||||
|
leading: const Icon(Icons.send_outlined),
|
||||||
|
title: const Text('Send To Device'),
|
||||||
|
children: devices.when(
|
||||||
|
data: (deviceList) {
|
||||||
|
final targets = deviceList
|
||||||
|
.where(
|
||||||
|
(device) => !device.isCurrentDevice && device.canSendTab,
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
return const [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.only(left: 72, right: 16),
|
||||||
|
title: Text('No target devices'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
|
.map(
|
||||||
|
(device) => ListTile(
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 72,
|
||||||
|
right: 16,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.devices_other, size: 18),
|
||||||
|
title: Text(device.displayName),
|
||||||
|
dense: true,
|
||||||
|
onTap: () async {
|
||||||
|
final tabState = ref.read(
|
||||||
|
tabStateProvider(selectedTabId),
|
||||||
|
);
|
||||||
|
if (tabState == null) return;
|
||||||
|
|
||||||
|
final title = tabState.title.isNotEmpty
|
||||||
|
? tabState.title
|
||||||
|
: tabState.url.toString();
|
||||||
|
|
||||||
|
final success = await ref
|
||||||
|
.read(syncRepositoryProvider.notifier)
|
||||||
|
.sendTabToDevice(
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
title: title,
|
||||||
|
url: tabState.url.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
if (success) {
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Sent tab to ${device.displayName}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ui_helper.showErrorMessage(
|
||||||
|
context,
|
||||||
|
'Failed to send tab',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
},
|
||||||
|
loading: () => const [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.only(left: 72, right: 16),
|
||||||
|
leading: Icon(Icons.devices_other, size: 18),
|
||||||
|
title: Text('Loading devices...'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
error: (_, _) => const [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.only(left: 72, right: 16),
|
||||||
|
title: Text('Failed to load devices'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,4 +128,13 @@ extension HitResultX on HitResult {
|
|||||||
hasLink &&
|
hasLink &&
|
||||||
tryGetLink()?.scheme == 'mailto';
|
tryGetLink()?.scheme == 'mailto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HitResult withCleanedLink(String cleanedUrl) {
|
||||||
|
return switch (this) {
|
||||||
|
UnknownHitResult() => UnknownHitResult(src: cleanedUrl),
|
||||||
|
ImageSrcHitResult(:final src) =>
|
||||||
|
ImageSrcHitResult(src: src, uri: cleanedUrl),
|
||||||
|
_ => this,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-22
@@ -37,8 +37,12 @@ import 'package:weblibre/features/geckoview/features/contextmenu/presentation/ca
|
|||||||
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_email.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_email.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_image.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_image.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_link.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/presentation/candidates/share_link.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/url_cleaner_tile.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart';
|
||||||
|
|
||||||
class ContextMenuDialog extends HookConsumerWidget {
|
class ContextMenuDialog extends HookConsumerWidget {
|
||||||
final HitResult hitResult;
|
final HitResult hitResult;
|
||||||
@@ -47,13 +51,46 @@ class ContextMenuDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final showContainerUi = ref.watch(
|
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||||
generalSettingsWithDefaultsProvider.select((s) => s.showContainerUi),
|
final showContainerUi = settings.showContainerUi;
|
||||||
|
final catalogAsync = ref.watch(urlCleanerCatalogServiceProvider);
|
||||||
|
|
||||||
|
final effectiveHitResult = useState(hitResult);
|
||||||
|
|
||||||
|
final url = hitResult.tryGetLink()?.toString();
|
||||||
|
final isCleanable = hitResult.isHttpLink();
|
||||||
|
final cleaner = useUrlCleanerController(
|
||||||
|
sourceUrl: isCleanable
|
||||||
|
? effectiveHitResult.value.tryGetLink()?.toString()
|
||||||
|
: null,
|
||||||
|
rules: catalogAsync.value,
|
||||||
|
cleanerEnabled: settings.urlCleanerEnabled,
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
|
autoApply: settings.urlCleanerAutoApply,
|
||||||
|
getCurrentUrl: () => effectiveHitResult.value.tryGetLink()?.toString(),
|
||||||
|
onApplyCleanedUrl: (cleanedUrl) {
|
||||||
|
effectiveHitResult.value = hitResult.withCleanedLink(cleanedUrl);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void applyCleanUrl() {
|
||||||
|
if (cleaner.applyCleanUrl()) {
|
||||||
|
showInfoMessage(context, 'URL cleaned');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void applySelectedTrackingRemovals(String previewUrl) {
|
||||||
|
if (cleaner.applyPreviewUrl(previewUrl)) {
|
||||||
|
showInfoMessage(context, 'URL preview applied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final effective = effectiveHitResult.value;
|
||||||
|
final showCleanerTile = isCleanable && cleaner.showTile;
|
||||||
|
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
title: AutoSizeText(
|
title: AutoSizeText(
|
||||||
hitResult.getTitle(),
|
effective.getTitle(),
|
||||||
minFontSize: 18,
|
minFontSize: 18,
|
||||||
maxFontSize: DefaultTextStyle.of(context).style.fontSize,
|
maxFontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
@@ -61,28 +98,37 @@ class ContextMenuDialog extends HookConsumerWidget {
|
|||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
if (OpenInNewTab.isSupported(hitResult))
|
if (showCleanerTile)
|
||||||
OpenInNewTab(hitResult: hitResult),
|
UrlCleanerTile(
|
||||||
if (showContainerUi && OpenInContainer.isSupported(hitResult))
|
result: cleaner.result!,
|
||||||
OpenInContainer(hitResult: hitResult),
|
currentUrl: effective.tryGetLink()?.toString() ?? url ?? '',
|
||||||
if (CopyLink.isSupported(hitResult)) CopyLink(hitResult: hitResult),
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
if (SaveFile.isSupported(hitResult)) SaveFile(hitResult: hitResult),
|
onClean: applyCleanUrl,
|
||||||
if (ShareLink.isSupported(hitResult)) ShareLink(hitResult: hitResult),
|
onApplySelectedRemovals: applySelectedTrackingRemovals,
|
||||||
if (ShareImage.isSupported(hitResult)) ShareImage(hitResult: hitResult),
|
applied: cleaner.applied,
|
||||||
if (OpenImageInNewTab.isSupported(hitResult))
|
),
|
||||||
OpenImageInNewTab(hitResult: hitResult),
|
if (OpenInNewTab.isSupported(effective))
|
||||||
if (CopyImage.isSupported(hitResult)) CopyImage(hitResult: hitResult),
|
OpenInNewTab(hitResult: effective),
|
||||||
if (SaveImage.isSupported(hitResult)) SaveImage(hitResult: hitResult),
|
if (showContainerUi && OpenInContainer.isSupported(effective))
|
||||||
if (CopyImageLocation.isSupported(hitResult))
|
OpenInContainer(hitResult: effective),
|
||||||
CopyImageLocation(hitResult: hitResult),
|
if (CopyLink.isSupported(effective)) CopyLink(hitResult: effective),
|
||||||
if (ShareEmail.isSupported(hitResult)) ShareEmail(hitResult: hitResult),
|
if (SaveFile.isSupported(effective)) SaveFile(hitResult: effective),
|
||||||
if (CopyEmail.isSupported(hitResult)) CopyEmail(hitResult: hitResult),
|
if (ShareLink.isSupported(effective)) ShareLink(hitResult: effective),
|
||||||
|
if (ShareImage.isSupported(effective)) ShareImage(hitResult: effective),
|
||||||
|
if (OpenImageInNewTab.isSupported(effective))
|
||||||
|
OpenImageInNewTab(hitResult: effective),
|
||||||
|
if (CopyImage.isSupported(effective)) CopyImage(hitResult: effective),
|
||||||
|
if (SaveImage.isSupported(effective)) SaveImage(hitResult: effective),
|
||||||
|
if (CopyImageLocation.isSupported(effective))
|
||||||
|
CopyImageLocation(hitResult: effective),
|
||||||
|
if (ShareEmail.isSupported(effective)) ShareEmail(hitResult: effective),
|
||||||
|
if (CopyEmail.isSupported(effective)) CopyEmail(hitResult: effective),
|
||||||
HookBuilder(
|
HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final isSupported = useCachedFuture(
|
final isSupported = useCachedFuture(
|
||||||
// ignore: discarded_futures useFuture
|
// ignore: discarded_futures useFuture
|
||||||
() => LaunchExternal.isSupported(hitResult),
|
() => LaunchExternal.isSupported(effective),
|
||||||
[hitResult],
|
[effective],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isSupported.data == false) {
|
if (isSupported.data == false) {
|
||||||
@@ -91,7 +137,7 @@ class ContextMenuDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
enabled: isSupported.connectionState != ConnectionState.done,
|
enabled: isSupported.connectionState != ConnectionState.done,
|
||||||
child: LaunchExternal(hitResult: hitResult),
|
child: LaunchExternal(hitResult: effective),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+30
-144
@@ -29,13 +29,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/core/design/app_colors.dart';
|
import 'package:weblibre/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/url_cleaner_result.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_unshortener_service.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/controllers/open_shared_content_unshorten_controller.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/controllers/open_shared_content_unshorten_controller.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/tracking_details_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/attribution_link.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/attribution_link.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/widgets/url_cleaner_tile.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.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/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
@@ -96,74 +95,33 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
supportedShortenerHosts.value ?? const <String>{},
|
supportedShortenerHosts.value ?? const <String>{},
|
||||||
);
|
);
|
||||||
|
|
||||||
// URL cleaner state
|
|
||||||
final cleanerResult = useState<UrlCleanerResult?>(null);
|
|
||||||
final cleanerApplied = useState(false);
|
|
||||||
|
|
||||||
final unshortenState = ref.watch(
|
final unshortenState = ref.watch(
|
||||||
openSharedContentUnshortenControllerProvider,
|
openSharedContentUnshortenControllerProvider,
|
||||||
);
|
);
|
||||||
|
final cleaner = useUrlCleanerController(
|
||||||
void runCleaner({bool allowAutoApply = false}) {
|
// Avoid expensive matching work on every keystroke.
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
sourceUrl: debouncedUrl.value,
|
||||||
|
rules: catalogAsync.value,
|
||||||
if (!settings.urlCleanerEnabled) return;
|
cleanerEnabled: settings.urlCleanerEnabled,
|
||||||
|
allowReferralMarketing: settings.urlCleanerAllowReferralMarketing,
|
||||||
final rules = catalogAsync.value;
|
autoApply: settings.urlCleanerAutoApply,
|
||||||
if (rules == null) return;
|
getCurrentUrl: () => textController.text,
|
||||||
|
onApplyCleanedUrl: (cleanedUrl) {
|
||||||
final result = cleanUrl(
|
textController.text = cleanedUrl;
|
||||||
currentUrl,
|
|
||||||
rules,
|
|
||||||
allowReferral: settings.urlCleanerAllowReferralMarketing,
|
|
||||||
);
|
|
||||||
cleanerResult.value = result;
|
|
||||||
|
|
||||||
// Only reset applied state when new tracking params are found,
|
|
||||||
// preserving the "cleaned" indicator when the URL is already clean.
|
|
||||||
if (result.removedParams.isNotEmpty) {
|
|
||||||
cleanerApplied.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowAutoApply && result.changed) {
|
|
||||||
textController.text = result.cleanedUrl;
|
|
||||||
cleanerApplied.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run URL cleaner on build when enabled and catalog is loaded.
|
|
||||||
// Uses debouncedUrl to avoid running regex matching on every keystroke.
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
runCleaner(allowAutoApply: settings.urlCleanerAutoApply);
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
[
|
|
||||||
debouncedUrl.value,
|
|
||||||
catalogAsync.value,
|
|
||||||
settings.urlCleanerEnabled,
|
|
||||||
settings.urlCleanerAllowReferralMarketing,
|
|
||||||
settings.urlCleanerAutoApply,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
void applyCleanUrl() {
|
void applyCleanUrl() {
|
||||||
final result = cleanerResult.value;
|
if (cleaner.applyCleanUrl()) {
|
||||||
if (result != null && result.changed) {
|
|
||||||
textController.text = result.cleanedUrl;
|
|
||||||
cleanerApplied.value = true;
|
|
||||||
showInfoMessage(context, 'URL cleaned');
|
showInfoMessage(context, 'URL cleaned');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void applySelectedTrackingRemovals(String previewUrl) {
|
void applySelectedTrackingRemovals(String previewUrl) {
|
||||||
if (previewUrl == textController.text) return;
|
if (cleaner.applyPreviewUrl(previewUrl)) {
|
||||||
|
|
||||||
textController.text = previewUrl;
|
|
||||||
cleanerApplied.value = true;
|
|
||||||
|
|
||||||
showInfoMessage(context, 'URL preview applied');
|
showInfoMessage(context, 'URL preview applied');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> openTab(TabMode tabMode) async {
|
Future<void> openTab(TabMode tabMode) async {
|
||||||
if (formKey.currentState?.validate() == true) {
|
if (formKey.currentState?.validate() == true) {
|
||||||
@@ -234,7 +192,12 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
16,
|
||||||
|
MediaQuery.of(context).viewInsets.bottom + 16,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -263,9 +226,8 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// URL Cleaner tile
|
// URL Cleaner tile
|
||||||
if (settings.urlCleanerEnabled &&
|
if (settings.urlCleanerEnabled && cleaner.result != null) ...[
|
||||||
cleanerResult.value != null) ...[
|
if (cleaner.result!.blocked)
|
||||||
if (cleanerResult.value!.blocked)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
MdiIcons.alertCircle,
|
MdiIcons.alertCircle,
|
||||||
@@ -274,20 +236,18 @@ class OpenSharedContent extends HookConsumerWidget {
|
|||||||
title: const Text('URL blocked by ClearURLs'),
|
title: const Text('URL blocked by ClearURLs'),
|
||||||
dense: true,
|
dense: true,
|
||||||
)
|
)
|
||||||
else if (cleanerResult.value!.removedParams.isNotEmpty)
|
else if (cleaner.result!.removedParams.isNotEmpty)
|
||||||
_UrlCleanerTile(
|
UrlCleanerTile(
|
||||||
result: cleanerResult.value!,
|
result: cleaner.result!,
|
||||||
currentUrl: currentUrl,
|
currentUrl: currentUrl,
|
||||||
allowReferralMarketing:
|
allowReferralMarketing:
|
||||||
settings.urlCleanerAllowReferralMarketing,
|
settings.urlCleanerAllowReferralMarketing,
|
||||||
onClean: cleanerResult.value!.changed
|
onClean: cleaner.result!.changed ? applyCleanUrl : null,
|
||||||
? applyCleanUrl
|
|
||||||
: null,
|
|
||||||
onApplySelectedRemovals: applySelectedTrackingRemovals,
|
onApplySelectedRemovals: applySelectedTrackingRemovals,
|
||||||
)
|
)
|
||||||
else if (cleanerApplied.value)
|
else if (cleaner.applied)
|
||||||
_UrlCleanerTile(
|
UrlCleanerTile(
|
||||||
result: cleanerResult.value!,
|
result: cleaner.result!,
|
||||||
currentUrl: currentUrl,
|
currentUrl: currentUrl,
|
||||||
allowReferralMarketing:
|
allowReferralMarketing:
|
||||||
settings.urlCleanerAllowReferralMarketing,
|
settings.urlCleanerAllowReferralMarketing,
|
||||||
@@ -525,80 +485,6 @@ Future<void> _showUnshortenerInfoDialog(BuildContext context) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UrlCleanerTile extends StatelessWidget {
|
|
||||||
final UrlCleanerResult result;
|
|
||||||
final String currentUrl;
|
|
||||||
final bool allowReferralMarketing;
|
|
||||||
final VoidCallback? onClean;
|
|
||||||
final ValueChanged<String>? onApplySelectedRemovals;
|
|
||||||
final bool applied;
|
|
||||||
|
|
||||||
const _UrlCleanerTile({
|
|
||||||
required this.result,
|
|
||||||
required this.currentUrl,
|
|
||||||
required this.allowReferralMarketing,
|
|
||||||
this.onClean,
|
|
||||||
this.onApplySelectedRemovals,
|
|
||||||
this.applied = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final paramCount = result.removedParams.length;
|
|
||||||
final hasParams = paramCount > 0;
|
|
||||||
|
|
||||||
final String subtitle;
|
|
||||||
if (!hasParams) {
|
|
||||||
subtitle = 'Tracking parameters removed';
|
|
||||||
} else if (paramCount == 1) {
|
|
||||||
subtitle = '1 tracking parameter found';
|
|
||||||
} else {
|
|
||||||
subtitle = '$paramCount tracking parameters found';
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
applied ? MdiIcons.checkCircle : MdiIcons.broom,
|
|
||||||
color: applied
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
title: Text(applied ? 'URL cleaned' : 'Tracking detected'),
|
|
||||||
subtitle: Text(subtitle),
|
|
||||||
trailing: applied || !hasParams
|
|
||||||
? null
|
|
||||||
: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 24, child: VerticalDivider(width: 16)),
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(MdiIcons.linkVariantRemove),
|
|
||||||
tooltip: 'Clean URL',
|
|
||||||
onPressed: onClean,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
dense: true,
|
|
||||||
onTap: hasParams
|
|
||||||
? () {
|
|
||||||
unawaited(
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => TrackingDetailsDialog(
|
|
||||||
currentUrl: currentUrl,
|
|
||||||
result: result,
|
|
||||||
allowReferralMarketing: allowReferralMarketing,
|
|
||||||
onApplySelectedRemovals: onApplySelectedRemovals,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OpenActionTile extends StatelessWidget {
|
class _OpenActionTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* 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_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/url_cleaner_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_rule.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_service.dart';
|
||||||
|
|
||||||
|
class UrlCleanerController {
|
||||||
|
final UrlCleanerResult? result;
|
||||||
|
final bool applied;
|
||||||
|
final bool Function() _applyCleanUrl;
|
||||||
|
final bool Function(String previewUrl) _applyPreviewUrl;
|
||||||
|
|
||||||
|
const UrlCleanerController._({
|
||||||
|
required this.result,
|
||||||
|
required this.applied,
|
||||||
|
required bool Function() applyCleanUrl,
|
||||||
|
required bool Function(String previewUrl) applyPreviewUrl,
|
||||||
|
}) : _applyCleanUrl = applyCleanUrl,
|
||||||
|
_applyPreviewUrl = applyPreviewUrl;
|
||||||
|
|
||||||
|
bool get showTile =>
|
||||||
|
result != null && (result!.removedParams.isNotEmpty || applied);
|
||||||
|
|
||||||
|
bool applyCleanUrl() => _applyCleanUrl();
|
||||||
|
|
||||||
|
bool applyPreviewUrl(String previewUrl) => _applyPreviewUrl(previewUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
UrlCleanerController useUrlCleanerController({
|
||||||
|
required String? sourceUrl,
|
||||||
|
required List<UrlCleanerRule>? rules,
|
||||||
|
required bool cleanerEnabled,
|
||||||
|
required bool allowReferralMarketing,
|
||||||
|
required bool autoApply,
|
||||||
|
required String? Function() getCurrentUrl,
|
||||||
|
required void Function(String cleanedUrl) onApplyCleanedUrl,
|
||||||
|
}) {
|
||||||
|
final cleanerResult = useState<UrlCleanerResult?>(null);
|
||||||
|
final cleanerApplied = useState(false);
|
||||||
|
|
||||||
|
final getCurrentUrlRef = useRef(getCurrentUrl);
|
||||||
|
getCurrentUrlRef.value = getCurrentUrl;
|
||||||
|
|
||||||
|
final onApplyCleanedUrlRef = useRef(onApplyCleanedUrl);
|
||||||
|
onApplyCleanedUrlRef.value = onApplyCleanedUrl;
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (!cleanerEnabled || sourceUrl == null) {
|
||||||
|
cleanerResult.value = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules == null) return null;
|
||||||
|
|
||||||
|
final result = cleanUrl(
|
||||||
|
sourceUrl,
|
||||||
|
rules,
|
||||||
|
allowReferral: allowReferralMarketing,
|
||||||
|
);
|
||||||
|
cleanerResult.value = result;
|
||||||
|
|
||||||
|
// Preserve the "already cleaned" state until a new removable parameter
|
||||||
|
// set is detected.
|
||||||
|
if (result.removedParams.isNotEmpty) {
|
||||||
|
cleanerApplied.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoApply && result.changed) {
|
||||||
|
onApplyCleanedUrlRef.value(result.cleanedUrl);
|
||||||
|
cleanerApplied.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [sourceUrl, rules, cleanerEnabled, allowReferralMarketing, autoApply]);
|
||||||
|
|
||||||
|
bool applyCleanUrl() {
|
||||||
|
final result = cleanerResult.value;
|
||||||
|
if (result == null || !result.changed) return false;
|
||||||
|
|
||||||
|
onApplyCleanedUrlRef.value(result.cleanedUrl);
|
||||||
|
cleanerApplied.value = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyPreviewUrl(String previewUrl) {
|
||||||
|
if (previewUrl == getCurrentUrlRef.value()) return false;
|
||||||
|
|
||||||
|
onApplyCleanedUrlRef.value(previewUrl);
|
||||||
|
cleanerApplied.value = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return UrlCleanerController._(
|
||||||
|
result: cleanerResult.value,
|
||||||
|
applied: cleanerApplied.value,
|
||||||
|
applyCleanUrl: applyCleanUrl,
|
||||||
|
applyPreviewUrl: applyPreviewUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/entities/url_cleaner_result.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/dialogs/tracking_details_dialog.dart';
|
||||||
|
|
||||||
|
class UrlCleanerTile extends StatelessWidget {
|
||||||
|
final UrlCleanerResult result;
|
||||||
|
final String currentUrl;
|
||||||
|
final bool allowReferralMarketing;
|
||||||
|
final VoidCallback? onClean;
|
||||||
|
final ValueChanged<String>? onApplySelectedRemovals;
|
||||||
|
final bool applied;
|
||||||
|
|
||||||
|
const UrlCleanerTile({
|
||||||
|
super.key,
|
||||||
|
required this.result,
|
||||||
|
required this.currentUrl,
|
||||||
|
required this.allowReferralMarketing,
|
||||||
|
this.onClean,
|
||||||
|
this.onApplySelectedRemovals,
|
||||||
|
this.applied = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final paramCount = result.removedParams.length;
|
||||||
|
final hasParams = paramCount > 0;
|
||||||
|
|
||||||
|
final String subtitle;
|
||||||
|
if (!hasParams) {
|
||||||
|
subtitle = 'Tracking parameters removed';
|
||||||
|
} else if (paramCount == 1) {
|
||||||
|
subtitle = '1 tracking parameter found';
|
||||||
|
} else {
|
||||||
|
subtitle = '$paramCount tracking parameters found';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
applied ? MdiIcons.checkCircle : MdiIcons.broom,
|
||||||
|
color: applied
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
title: Text(applied ? 'URL cleaned' : 'Tracking detected'),
|
||||||
|
subtitle: Text(subtitle),
|
||||||
|
trailing: applied || !hasParams
|
||||||
|
? null
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 24, child: VerticalDivider(width: 16)),
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(MdiIcons.linkVariantRemove),
|
||||||
|
tooltip: 'Clean URL',
|
||||||
|
onPressed: onClean,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
onTap: hasParams
|
||||||
|
? () {
|
||||||
|
unawaited(
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => TrackingDetailsDialog(
|
||||||
|
currentUrl: currentUrl,
|
||||||
|
result: result,
|
||||||
|
allowReferralMarketing: allowReferralMarketing,
|
||||||
|
onApplySelectedRemovals: onApplySelectedRemovals,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user