show decoded url's to the user
This commit is contained in:
@@ -41,4 +41,59 @@ extension UriX on Uri {
|
|||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get displayPath {
|
||||||
|
if (path.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final segments = pathSegments.join('/');
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
buffer.write('/');
|
||||||
|
}
|
||||||
|
buffer.write(segments);
|
||||||
|
if (path.length > 1 && path.endsWith('/')) {
|
||||||
|
buffer.write('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get displayString {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
if (scheme.isNotEmpty) {
|
||||||
|
buffer.write('$scheme:');
|
||||||
|
}
|
||||||
|
if (authority.isNotEmpty) {
|
||||||
|
buffer
|
||||||
|
..write('//')
|
||||||
|
..write(authority);
|
||||||
|
}
|
||||||
|
buffer.write(displayPath);
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
buffer
|
||||||
|
..write('?')
|
||||||
|
..write(query);
|
||||||
|
}
|
||||||
|
if (fragment.isNotEmpty) {
|
||||||
|
buffer
|
||||||
|
..write('#')
|
||||||
|
..write(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UriStringX on String {
|
||||||
|
String get uriDisplayString {
|
||||||
|
final uri = Uri.tryParse(this);
|
||||||
|
if (uri == null || uri.toString() != this) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return uri.displayString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -29,6 +29,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.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/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
||||||
@@ -271,7 +272,7 @@ class _ShareHeader extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
url,
|
url.uriDisplayString,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/utils/open_in_custom_tab.dart';
|
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/utils/open_in_custom_tab.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||||
@@ -203,7 +204,7 @@ class _AttributionLinkRow extends StatelessWidget {
|
|||||||
width: 96,
|
width: 96,
|
||||||
child: Text(label, style: Theme.of(context).textTheme.bodySmall),
|
child: Text(label, style: Theme.of(context).textTheme.bodySmall),
|
||||||
),
|
),
|
||||||
Expanded(child: Text(url, style: linkStyle)),
|
Expanded(child: Text(url.uriDisplayString, style: linkStyle)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.open_in_new,
|
Icons.open_in_new,
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart';
|
import 'package:weblibre/features/settings/presentation/dialogs/delete_all_exceptions_dialog.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
@@ -151,7 +152,7 @@ class _ExceptionTile extends StatelessWidget {
|
|||||||
leading: uri != null
|
leading: uri != null
|
||||||
? UrlIcon([uri], iconSize: 24)
|
? UrlIcon([uri], iconSize: 24)
|
||||||
: const Icon(MdiIcons.shieldOutline),
|
: const Icon(MdiIcons.shieldOutline),
|
||||||
title: Text(exception.url),
|
title: Text(exception.url.uriDisplayString),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: onDelete,
|
onPressed: onDelete,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
import 'package:weblibre/features/user/data/models/engine_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/features/user/domain/repositories/engine_settings.dart';
|
||||||
@@ -112,7 +113,7 @@ class DohSettingsContent extends HookConsumerWidget {
|
|||||||
(provider) => RadioListTile.adaptive(
|
(provider) => RadioListTile.adaptive(
|
||||||
value: provider.url,
|
value: provider.url,
|
||||||
title: Text(provider.name),
|
title: Text(provider.name),
|
||||||
subtitle: Text(provider.url),
|
subtitle: Text(provider.url.uriDisplayString),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
import 'package:weblibre/features/web_feed/domain/providers.dart';
|
import 'package:weblibre/features/web_feed/domain/providers.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ class SelectFeedDialog extends HookConsumerWidget {
|
|||||||
title: Text(
|
title: Text(
|
||||||
data.feedData.title.whenNotEmpty ?? 'Unnamed Feed',
|
data.feedData.title.whenNotEmpty ?? 'Unnamed Feed',
|
||||||
),
|
),
|
||||||
subtitle: Text(uri.toString()),
|
subtitle: Text(uri.displayString),
|
||||||
trailing: const Icon(Icons.add),
|
trailing: const Icon(Icons.add),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
FeedCreateRoute(feedId: uri).pushReplacement(context);
|
FeedCreateRoute(feedId: uri).pushReplacement(context);
|
||||||
@@ -72,7 +73,7 @@ class SelectFeedDialog extends HookConsumerWidget {
|
|||||||
loading: () => Skeletonizer(
|
loading: () => Skeletonizer(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(BoneMock.title),
|
title: Text(BoneMock.title),
|
||||||
subtitle: Skeleton.keep(child: Text(uri.toString())),
|
subtitle: Skeleton.keep(child: Text(uri.displayString)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fading_scroll/fading_scroll.dart';
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
|
|
||||||
class UriBreadcrumb extends StatelessWidget {
|
class UriBreadcrumb extends StatelessWidget {
|
||||||
final Uri uri;
|
final Uri uri;
|
||||||
@@ -37,8 +38,10 @@ class UriBreadcrumb extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final pathSegments = uri.pathSegments;
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: uri.toString(),
|
message: uri.displayString,
|
||||||
onTriggered: onTooltipTriggered,
|
onTriggered: onTooltipTriggered,
|
||||||
child: DefaultTextStyle(
|
child: DefaultTextStyle(
|
||||||
style: style ?? DefaultTextStyle.of(context).style,
|
style: style ?? DefaultTextStyle.of(context).style,
|
||||||
@@ -59,9 +62,9 @@ class UriBreadcrumb extends StatelessWidget {
|
|||||||
overflow: TextOverflow.visible,
|
overflow: TextOverflow.visible,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
if (uri.pathSegments.any((s) => s.isNotEmpty))
|
if (pathSegments.any((s) => s.isNotEmpty))
|
||||||
Text(
|
Text(
|
||||||
' › ${uri.pathSegments.whereNot((s) => s.isEmpty).join(' › ')}',
|
' › ${pathSegments.whereNot((s) => s.isEmpty).join(' › ')}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
overflow: TextOverflow.visible,
|
overflow: TextOverflow.visible,
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:weblibre/extensions/uri.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Uri display formatting', () {
|
||||||
|
test('decodes path segments for read-only display', () {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'https://example.com/hello%20world/caf%C3%A9?greeting=Ol%C3%A1%20mundo#frag%20ment',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
uri.displayString,
|
||||||
|
'https://example.com/hello world/café?greeting=Ol%C3%A1%20mundo#frag%20ment',
|
||||||
|
);
|
||||||
|
expect(uri.pathSegments, ['hello world', 'café']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses decoded path segments', () {
|
||||||
|
final uri = Uri.parse('https://example.com/path%2Fwith%2Fslash');
|
||||||
|
|
||||||
|
expect(uri.displayString, 'https://example.com/path/with/slash');
|
||||||
|
expect(uri.pathSegments, ['path/with/slash']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps query encoded', () {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'https://example.com/?q=fish%20%26%20chips&redirect=a%2Fb',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
uri.displayString,
|
||||||
|
'https://example.com/?q=fish%20%26%20chips&redirect=a%2Fb',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses authority as-is for display', () {
|
||||||
|
final uri = Uri.parse('https://user%20name@example.com/path');
|
||||||
|
|
||||||
|
expect(uri.authority, 'user%20name@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leaves malformed escapes unchanged', () {
|
||||||
|
expect(
|
||||||
|
'https://example.com/%ZZ?query=%'.uriDisplayString,
|
||||||
|
'https://example.com/%ZZ?query=%',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user