Allow Tor Caching + Our Own Linkify
continuous-integration/drone/pr Build is pending
Details
continuous-integration/drone/pr Build is pending
Details
This commit is contained in:
parent
9d10b9ea8d
commit
c6e64a3a5f
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "de",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "es",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "fr",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "it",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pl",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "pt",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "ru",
|
"@@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",
|
"labelTorNetwork": "Tor Network",
|
||||||
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
|
||||||
"labelACNCircuitInfo": "ACN Circuit Info",
|
"labelACNCircuitInfo": "ACN Circuit Info",
|
||||||
|
|
|
@ -45,6 +45,10 @@ class Settings extends ChangeNotifier {
|
||||||
int _socksPort = -1;
|
int _socksPort = -1;
|
||||||
int _controlPort = -1;
|
int _controlPort = -1;
|
||||||
String _customTorAuth = "";
|
String _customTorAuth = "";
|
||||||
|
bool _useTorCache = false;
|
||||||
|
String _torCacheDir = "";
|
||||||
|
|
||||||
|
String get torCacheDir => _torCacheDir;
|
||||||
|
|
||||||
void setTheme(String themeId, String mode) {
|
void setTheme(String themeId, String mode) {
|
||||||
theme = getTheme(themeId, mode);
|
theme = getTheme(themeId, mode);
|
||||||
|
@ -99,6 +103,8 @@ class Settings extends ChangeNotifier {
|
||||||
_customTorConfig = settings["CustomTorrc"] ?? "";
|
_customTorConfig = settings["CustomTorrc"] ?? "";
|
||||||
_socksPort = settings["CustomSocksPort"] ?? -1;
|
_socksPort = settings["CustomSocksPort"] ?? -1;
|
||||||
_controlPort = settings["CustomControlPort"] ?? -1;
|
_controlPort = settings["CustomControlPort"] ?? -1;
|
||||||
|
_useTorCache = settings["UseTorCache"] ?? false;
|
||||||
|
_torCacheDir = settings["TorCacheDir"] ?? "";
|
||||||
|
|
||||||
// Push the experimental settings to Consumers of Settings
|
// Push the experimental settings to Consumers of Settings
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -252,6 +258,12 @@ class Settings extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get useTorCache => _useTorCache;
|
||||||
|
set useTorCache(bool useTorCache) {
|
||||||
|
_useTorCache = useTorCache;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// Settings / Gettings for setting the custom tor config..
|
// Settings / Gettings for setting the custom tor config..
|
||||||
String get torConfig => _customTorConfig;
|
String get torConfig => _customTorConfig;
|
||||||
set torConfig(String torConfig) {
|
set torConfig(String torConfig) {
|
||||||
|
@ -304,6 +316,10 @@ class Settings extends ChangeNotifier {
|
||||||
"CustomSocksPort": _socksPort,
|
"CustomSocksPort": _socksPort,
|
||||||
"CustomControlPort": _controlPort,
|
"CustomControlPort": _controlPort,
|
||||||
"CustomAuth": _customTorAuth,
|
"CustomAuth": _customTorAuth,
|
||||||
|
"UseTorCache": _useTorCache,
|
||||||
|
"TorCacheDir": _torCacheDir
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,380 @@
|
||||||
|
//
|
||||||
|
// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ and
|
||||||
|
// subsequently modified...
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// 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 LinkableSpan(
|
||||||
|
mouseCursor: SystemMouseCursors.click,
|
||||||
|
inlineSpan: TextSpan(
|
||||||
|
text: element.text,
|
||||||
|
style: linkStyle,
|
||||||
|
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return TextSpan(
|
||||||
|
text: element.text,
|
||||||
|
style: linkStyle,
|
||||||
|
recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return TextSpan(
|
||||||
|
text: element.text,
|
||||||
|
style: style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Originally from linkify
|
||||||
|
// 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 'package:cwtch/third_party/linkify/uri.dart';
|
||||||
|
|
||||||
|
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 {
|
||||||
|
/// Removes http/https from shown URLs.
|
||||||
|
final bool humanize;
|
||||||
|
|
||||||
|
/// Removes www. from shown URLs.
|
||||||
|
final bool removeWww;
|
||||||
|
|
||||||
|
/// 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.humanize = true,
|
||||||
|
this.removeWww = false,
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
// Originally from linkify
|
||||||
|
// 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 'package:cwtch/third_party/linkify/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;
|
||||||
|
|
||||||
|
// We do not, ever, change the original text of a message.
|
||||||
|
if (options.defaultToHttps) {
|
||||||
|
url = url.replaceFirst('http://', 'https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
// These options are intended for the human-readable portion of
|
||||||
|
// the URI
|
||||||
|
if (options.humanize) {
|
||||||
|
originalUrl = originalUrl.replaceFirst(RegExp(r'https?://'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.removeWww) {
|
||||||
|
originalUrl = originalUrl.replaceFirst(RegExp(r'www\.'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
list.add(UrlElement(originalUrl, url));
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
|
@ -76,6 +76,18 @@ class _TorStatusView extends State<TorStatusView> {
|
||||||
subtitle: SelectableText(torStatus.version),
|
subtitle: SelectableText(torStatus.version),
|
||||||
leading: Icon(CwtchIcons.info_24px, color: settings.current().mainTextColor),
|
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(
|
SwitchListTile(
|
||||||
title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced),
|
title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription),
|
subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription),
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cwtch/models/message.dart';
|
import 'package:cwtch/models/message.dart';
|
||||||
|
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
|
||||||
|
import 'package:cwtch/third_party/linkify/linkify.dart';
|
||||||
|
import 'package:cwtch/third_party/linkify/uri.dart';
|
||||||
import 'package:cwtch/widgets/malformedbubble.dart';
|
import 'package:cwtch/widgets/malformedbubble.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -8,7 +11,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../model.dart';
|
import '../model.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../settings.dart';
|
import '../settings.dart';
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -125,13 +125,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_localizations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -196,13 +189,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.6.3"
|
||||||
linkify:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: linkify
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -43,7 +43,6 @@ dependencies:
|
||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||||
file_picker: ^4.0.1
|
file_picker: ^4.0.1
|
||||||
file_picker_desktop: ^1.1.0
|
file_picker_desktop: ^1.1.0
|
||||||
flutter_linkify: ^5.0.2
|
|
||||||
url_launcher: ^6.0.12
|
url_launcher: ^6.0.12
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
Loading…
Reference in New Issue