Merge pull request 'Allow Tor Caching + Our Own Linkify' (#316) from torcache into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #316
Reviewed-by: erinn <erinn@openprivacy.ca>
This commit is contained in:
erinn 2022-01-18 23:06:25 +00:00
commit e99fc45a28
19 changed files with 714 additions and 27 deletions

View File

@ -1 +1 @@
2022-01-17-17-19-v1.5.4-5-g4cf95d6
2022-01-18-16-33-v1.5.4-8-g2aea700

View File

@ -1 +1 @@
2022-01-17-22-19-v1.5.4-5-g4cf95d6
2022-01-18-21-29-v1.5.4-8-g2aea700

View File

@ -1,6 +1,8 @@
{
"@@locale": "de",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "en",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "es",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "fr",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "it",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "pl",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "pt",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -1,6 +1,8 @@
{
"@@locale": "ru",
"@@last_modified": "2022-01-17T21:20:54+01:00",
"@@last_modified": "2022-01-18T00:38:14+01:00",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",

View File

@ -116,4 +116,26 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''');
yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)");
yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], '''MIT License
Copyright (c) 2019/2020 Charles-William Crete
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''');
}

View File

@ -45,6 +45,10 @@ class Settings extends ChangeNotifier {
int _socksPort = -1;
int _controlPort = -1;
String _customTorAuth = "";
bool _useTorCache = false;
String _torCacheDir = "";
String get torCacheDir => _torCacheDir;
void setTheme(String themeId, String mode) {
theme = getTheme(themeId, mode);
@ -99,6 +103,8 @@ class Settings extends ChangeNotifier {
_customTorConfig = settings["CustomTorrc"] ?? "";
_socksPort = settings["CustomSocksPort"] ?? -1;
_controlPort = settings["CustomControlPort"] ?? -1;
_useTorCache = settings["UseTorCache"] ?? false;
_torCacheDir = settings["TorCacheDir"] ?? "";
// Push the experimental settings to Consumers of Settings
notifyListeners();
@ -252,6 +258,12 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
bool get useTorCache => _useTorCache;
set useTorCache(bool useTorCache) {
_useTorCache = useTorCache;
notifyListeners();
}
// Settings / Gettings for setting the custom tor config..
String get torConfig => _customTorConfig;
set torConfig(String torConfig) {
@ -304,6 +316,8 @@ class Settings extends ChangeNotifier {
"CustomSocksPort": _socksPort,
"CustomControlPort": _controlPort,
"CustomAuth": _customTorAuth,
"UseTorCache": _useTorCache,
"TorCacheDir": _torCacheDir
};
}
}

View File

@ -0,0 +1,399 @@
// Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart
//
// Now uses local `linkify`
//
// Original License for this code:
// MIT License
// Copyright (c) 2020 Charles-William Crete
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'linkify.dart';
export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier;
/// Callback clicked link
typedef LinkCallback = void Function(LinkableElement link);
/// Turns URLs into links
class Linkify extends StatelessWidget {
/// Text to be linkified
final String text;
/// Linkifiers to be used for linkify
final List<Linkifier> linkifiers;
/// Callback for tapping a link
final LinkCallback? onOpen;
/// linkify's options.
final LinkifyOptions options;
// TextSpan
/// Style for non-link text
final TextStyle? style;
/// Style of link text
final TextStyle? linkStyle;
// Text.rich
/// How the text should be aligned horizontally.
final TextAlign textAlign;
/// Text direction of the text
final TextDirection? textDirection;
/// The maximum number of lines for the text to span, wrapping if necessary
final int? maxLines;
/// How visual overflow should be handled.
final TextOverflow overflow;
/// The number of font pixels for each logical pixel
final double textScaleFactor;
/// Whether the text should break at soft line breaks.
final bool softWrap;
/// The strut style used for the vertical layout
final StrutStyle? strutStyle;
/// Used to select a font when the same Unicode character can
/// be rendered differently, depending on the locale
final Locale? locale;
/// Defines how to measure the width of the rendered text.
final TextWidthBasis textWidthBasis;
/// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line.
final TextHeightBehavior? textHeightBehavior;
const Linkify({
Key? key,
required this.text,
this.linkifiers = defaultLinkifiers,
this.onOpen,
this.options = const LinkifyOptions(),
// TextSpan
this.style,
this.linkStyle,
// RichText
this.textAlign = TextAlign.start,
this.textDirection,
this.maxLines,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.softWrap = true,
this.strutStyle,
this.locale,
this.textWidthBasis = TextWidthBasis.parent,
this.textHeightBehavior,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final elements = linkify(
text,
options: options,
linkifiers: linkifiers,
);
return Text.rich(
buildTextSpan(
elements,
style: Theme.of(context).textTheme.bodyText2?.merge(style),
onOpen: onOpen,
useMouseRegion: true,
linkStyle: Theme.of(context)
.textTheme
.bodyText2
?.merge(style)
.copyWith(
color: Colors.blueAccent,
decoration: TextDecoration.underline,
)
.merge(linkStyle),
),
textAlign: textAlign,
textDirection: textDirection,
maxLines: maxLines,
overflow: overflow,
textScaleFactor: textScaleFactor,
softWrap: softWrap,
strutStyle: strutStyle,
locale: locale,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
);
}
}
/// Turns URLs into links
class SelectableLinkify extends StatelessWidget {
/// Text to be linkified
final String text;
/// The number of font pixels for each logical pixel
final textScaleFactor;
/// Linkifiers to be used for linkify
final List<Linkifier> linkifiers;
/// Callback for tapping a link
final LinkCallback? onOpen;
/// linkify's options.
final LinkifyOptions options;
// TextSpan
/// Style for non-link text
final TextStyle? style;
/// Style of link text
final TextStyle? linkStyle;
// Text.rich
/// How the text should be aligned horizontally.
final TextAlign? textAlign;
/// Text direction of the text
final TextDirection? textDirection;
/// The minimum number of lines to occupy when the content spans fewer lines.
final int? minLines;
/// The maximum number of lines for the text to span, wrapping if necessary
final int? maxLines;
/// The strut style used for the vertical layout
final StrutStyle? strutStyle;
/// Defines how to measure the width of the rendered text.
final TextWidthBasis? textWidthBasis;
// SelectableText.rich
/// Defines the focus for this widget.
final FocusNode? focusNode;
/// Whether to show cursor
final bool showCursor;
/// Whether this text field should focus itself if nothing else is already focused.
final bool autofocus;
/// Configuration of toolbar options
final ToolbarOptions? toolbarOptions;
/// How thick the cursor will be
final double cursorWidth;
/// How rounded the corners of the cursor should be
final Radius? cursorRadius;
/// The color to use when painting the cursor
final Color? cursorColor;
/// Determines the way that drag start behavior is handled
final DragStartBehavior dragStartBehavior;
/// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu,
/// and tapping will move the text caret
final bool enableInteractiveSelection;
/// Called when the user taps on this selectable text (not link)
final GestureTapCallback? onTap;
final ScrollPhysics? scrollPhysics;
/// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line.
final TextHeightBehavior? textHeightBehavior;
/// How tall the cursor will be.
final double? cursorHeight;
/// Optional delegate for building the text selection handles and toolbar.
final TextSelectionControls? selectionControls;
/// Called when the user changes the selection of text (including the cursor location).
final SelectionChangedCallback? onSelectionChanged;
const SelectableLinkify({
Key? key,
required this.text,
this.linkifiers = defaultLinkifiers,
this.onOpen,
this.options = const LinkifyOptions(),
// TextSpan
this.style,
this.linkStyle,
// RichText
this.textAlign,
this.textDirection,
this.minLines,
this.maxLines,
// SelectableText
this.focusNode,
this.textScaleFactor = 1.0,
this.strutStyle,
this.showCursor = false,
this.autofocus = false,
this.toolbarOptions,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.onTap,
this.scrollPhysics,
this.textWidthBasis,
this.textHeightBehavior,
this.cursorHeight,
this.selectionControls,
this.onSelectionChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final elements = linkify(
text,
options: options,
linkifiers: linkifiers,
);
return SelectableText.rich(
buildTextSpan(
elements,
style: Theme.of(context).textTheme.bodyText2?.merge(style),
onOpen: onOpen,
linkStyle: Theme.of(context)
.textTheme
.bodyText2
?.merge(style)
.copyWith(
color: Colors.blueAccent,
decoration: TextDecoration.underline,
)
.merge(linkStyle),
),
textAlign: textAlign,
textDirection: textDirection,
minLines: minLines,
maxLines: maxLines,
focusNode: focusNode,
strutStyle: strutStyle,
showCursor: showCursor,
textScaleFactor: textScaleFactor,
autofocus: autofocus,
toolbarOptions: toolbarOptions,
cursorWidth: cursorWidth,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
dragStartBehavior: dragStartBehavior,
enableInteractiveSelection: enableInteractiveSelection,
onTap: onTap,
scrollPhysics: scrollPhysics,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
cursorHeight: cursorHeight,
selectionControls: selectionControls,
onSelectionChanged: onSelectionChanged,
);
}
}
class LinkableSpan extends WidgetSpan {
LinkableSpan({
required MouseCursor mouseCursor,
required InlineSpan inlineSpan,
}) : super(
child: MouseRegion(
cursor: mouseCursor,
child: Text.rich(
inlineSpan,
),
),
);
}
/// Raw TextSpan builder for more control on the RichText
TextSpan buildTextSpan(
List<LinkifyElement> elements, {
TextStyle? style,
TextStyle? linkStyle,
LinkCallback? onOpen,
bool useMouseRegion = false,
}) {
return TextSpan(
children: elements.map<InlineSpan>(
(element) {
if (element is LinkableElement) {
if (useMouseRegion) {
return TooltipSpan(
message: element.url,
inlineSpan: LinkableSpan(
mouseCursor: SystemMouseCursors.click,
inlineSpan: TextSpan(
text: element.text,
style: linkStyle,
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
),
));
} else {
return TooltipSpan(
message: element.url,
inlineSpan: TextSpan(
text: element.text,
style: linkStyle,
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
));
}
} else {
return TextSpan(
text: element.text,
style: style,
);
}
},
).toList(),
);
}
// Show a tooltip over an inlined element in a Rich Text widget.
class TooltipSpan extends WidgetSpan {
TooltipSpan({
required String message,
required InlineSpan inlineSpan,
}) : super(
child: Tooltip(
message: message,
child: Text.rich(
inlineSpan,
),
),
);
}

121
lib/third_party/linkify/linkify.dart vendored Normal file
View File

@ -0,0 +1,121 @@
// Originally from linkify https://github.com/Cretezy/linkify/blob/ba536fa85e7e3a16e580f153616f399458986183/lib/linkify.dart
// Removed options `removeWWW` and `humanize`
//
// MIT License
//
// Copyright (c) 2019 Charles-William Crete
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import 'uri.dart';
export 'uri.dart' show UrlLinkifier, UrlElement;
abstract class LinkifyElement {
final String text;
LinkifyElement(this.text);
@override
bool operator ==(other) => equals(other);
bool equals(other) => other is LinkifyElement && other.text == text;
}
class LinkableElement extends LinkifyElement {
final String url;
LinkableElement(String? text, this.url) : super(text ?? url);
@override
bool operator ==(other) => equals(other);
@override
bool equals(other) => other is LinkableElement && super.equals(other) && other.url == url;
}
/// Represents an element containing text
class TextElement extends LinkifyElement {
TextElement(String text) : super(text);
@override
String toString() {
return "TextElement: '$text'";
}
@override
bool operator ==(other) => equals(other);
@override
bool equals(other) => other is TextElement && super.equals(other);
}
abstract class Linkifier {
const Linkifier();
List<LinkifyElement> parse(List<LinkifyElement> elements, LinkifyOptions options);
}
class LinkifyOptions {
/// Enables loose URL parsing (any string with "." is a URL).
final bool looseUrl;
/// When used with [looseUrl], default to `https` instead of `http`.
final bool defaultToHttps;
/// Excludes `.` at end of URLs.
final bool excludeLastPeriod;
const LinkifyOptions({
this.looseUrl = false,
this.defaultToHttps = false,
this.excludeLastPeriod = true,
});
}
const _urlLinkifier = UrlLinkifier();
const defaultLinkifiers = [_urlLinkifier];
/// Turns [text] into a list of [LinkifyElement]
///
/// Use [humanize] to remove http/https from the start of the URL shown.
/// Will default to `false` (if `null`)
///
/// Uses [linkTypes] to enable some types of links (URL, email).
/// Will default to all (if `null`).
List<LinkifyElement> linkify(
String text, {
LinkifyOptions options = const LinkifyOptions(),
List<Linkifier> linkifiers = defaultLinkifiers,
}) {
var list = <LinkifyElement>[TextElement(text)];
if (text.isEmpty) {
return [];
}
if (linkifiers.isEmpty) {
return list;
}
linkifiers.forEach((linkifier) {
list = linkifier.parse(list, options);
});
return list;
}

118
lib/third_party/linkify/uri.dart vendored Normal file
View File

@ -0,0 +1,118 @@
// Originally from linkify: https://github.com/Cretezy/linkify/blob/dfb3e43b0e56452bad584ddb0bf9b73d8db0589f/lib/src/url.dart
//
// Removed handling of `removeWWW` and `humanize`.
// Removed auto-appending of `http(s)://` to the readable url
//
// MIT License
//
// Copyright (c) 2019 Charles-William Crete
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import 'linkify.dart';
final _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)',
caseSensitive: false,
dotAll: true,
);
final _looseUrlRegex = RegExp(
r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))',
caseSensitive: false,
dotAll: true,
);
final _protocolIdentifierRegex = RegExp(
r'^(https?:\/\/)',
caseSensitive: false,
);
class UrlLinkifier extends Linkifier {
const UrlLinkifier();
@override
List<LinkifyElement> parse(elements, options) {
final list = <LinkifyElement>[];
elements.forEach((element) {
if (element is TextElement) {
var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
if (match == null) {
list.add(element);
} else {
final text = element.text.replaceFirst(match.group(0)!, '');
if (match.group(1)?.isNotEmpty == true) {
list.add(TextElement(match.group(1)!));
}
if (match.group(2)?.isNotEmpty == true) {
var originalUrl = match.group(2)!;
String? end;
if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
end = ".";
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
}
var url = originalUrl;
// If protocol has not been specified then append a protocol
// to the start of the URL so that it can be opened...
if (!url.startsWith("https://") && !url.startsWith("http://")) {
url = "https://" + url;
}
list.add(UrlElement(url, originalUrl));
if (end != null) {
list.add(TextElement(end));
}
}
if (text.isNotEmpty) {
list.addAll(parse([TextElement(text)], options));
}
}
} else {
list.add(element);
}
});
return list;
}
}
/// Represents an element containing a link
class UrlElement extends LinkableElement {
UrlElement(String url, [String? text]) : super(text, url);
@override
String toString() {
return "LinkElement: '$url' ($text)";
}
@override
bool operator ==(other) => equals(other);
@override
bool equals(other) => other is UrlElement && super.equals(other);
}

View File

@ -76,6 +76,18 @@ class _TorStatusView extends State<TorStatusView> {
subtitle: SelectableText(torStatus.version),
leading: Icon(CwtchIcons.info_24px, color: settings.current().mainTextColor),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.torSettingsEnableCache),
subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledCacheDescription),
value: settings.useTorCache,
onChanged: (bool value) {
settings.useTorCache = value;
saveSettings(context);
},
activeTrackColor: settings.theme.defaultButtonColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.cached, color: settings.current().mainTextColor),
),
SwitchListTile(
title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced),
subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription),

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -8,7 +9,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import 'package:intl/intl.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart';
@ -67,7 +67,7 @@ class MessageBubbleState extends State<MessageBubble> {
wdgMessage = SelectableLinkify(
text: widget.content + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
options: LinkifyOptions(humanize: false, removeWww: false, looseUrl: true, defaultToHttps: true),
options: LinkifyOptions(looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: (link) {
_modalOpenLink(context, link);

View File

@ -125,13 +125,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_linkify:
dependency: "direct main"
description:
name: flutter_linkify
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -196,13 +189,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
linkify:
dependency: transitive
description:
name: linkify
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
matcher:
dependency: transitive
description:

View File

@ -43,7 +43,6 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.0.1
file_picker_desktop: ^1.1.0
flutter_linkify: ^5.0.2
url_launcher: ^6.0.12
dev_dependencies: