Compare commits

..

5 Commits

50 changed files with 855 additions and 1078 deletions

View File

@ -8,7 +8,7 @@ clone:
steps: steps:
- name: clone - name: clone
image: cirrusci/flutter:2.8.0 image: cirrusci/flutter:3.0.1
environment: environment:
buildbot_key_b64: buildbot_key_b64:
from_secret: buildbot_key_b64 from_secret: buildbot_key_b64
@ -24,7 +24,7 @@ steps:
- git checkout $DRONE_COMMIT - git checkout $DRONE_COMMIT
- name: fetch - name: fetch
image: cirrusci/flutter:2.8.0 image: cirrusci/flutter:3.0.1
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache
@ -47,7 +47,7 @@ steps:
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting # #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
- name: build-linux - name: build-linux
image: openpriv/flutter-desktop:linux-fstable-2.8.0 image: openpriv/flutter-desktop:linux-fstable-3.0.1-rc1
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache
@ -61,7 +61,7 @@ steps:
- rm -r cwtch - rm -r cwtch
- name: test-build-android - name: test-build-android
image: cirrusci/flutter:2.8.0 image: cirrusci/flutter:3.0.1
when: when:
event: pull_request event: pull_request
volumes: volumes:
@ -71,7 +71,7 @@ steps:
- flutter build apk --debug - flutter build apk --debug
- name: build-android - name: build-android
image: cirrusci/flutter:2.8.0 image: cirrusci/flutter:3.0.1
when: when:
event: push event: push
environment: environment:
@ -95,7 +95,7 @@ steps:
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android #- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
- name: widget-tests - name: widget-tests
image: cirrusci/flutter:2.8.0 image: cirrusci/flutter:3.0.1
volumes: volumes:
- name: deps - name: deps
path: /root/.pub-cache path: /root/.pub-cache

7
.gitignore vendored
View File

@ -40,17 +40,10 @@ app.*.symbols
# Obfuscation related # Obfuscation related
app.*.map.json app.*.map.json
# Tor
data-dir*
# Compiled Libs
linux/tor linux/tor
linux/libCwtch.so linux/libCwtch.so
android/cwtch/cwtch.aar android/cwtch/cwtch.aar
android/app/src/main/jniLibs/*/libtor.so android/app/src/main/jniLibs/*/libtor.so
libCwtch.dylib
coverage coverage
test/failures test/failures
.gradle .gradle

View File

@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
compileSdkVersion 30 compileSdkVersion 31
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
@ -48,7 +48,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "im.cwtch.flwtch" applicationId "im.cwtch.flwtch"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@ -1,16 +1,21 @@
// Generated file. // Generated file.
//
// If you wish to remove Flutter's multidex support, delete this entire file. // If you wish to remove Flutter's multidex support, delete this entire file.
//
// Modifications to this file should be done in a copy under a different name
// as this file may be regenerated.
package io.flutter.app; package io.flutter.app;
import android.app.Application;
import android.content.Context; import android.content.Context;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.multidex.MultiDex; import androidx.multidex.MultiDex;
/** /**
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support. * Extension of {@link android.app.Application}, adding multidex support.
*/ */
public class FlutterMultiDexApplication extends FlutterApplication { public class FlutterMultiDexApplication extends Application {
@Override @Override
@CallSuper @CallSuper
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {

View File

@ -536,10 +536,15 @@ class MainActivity: FlutterActivity() {
Log.i("MainActivity.kt", "onResume") Log.i("MainActivity.kt", "onResume")
if (myReceiver == null) { if (myReceiver == null) {
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder") Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) val bm = flutterEngine?.dartExecutor?.binaryMessenger;
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") if (bm != null) {
myReceiver = MyBroadcastReceiver(mc) val mc = MethodChannel(bm, CWTCH_EVENTBUS)
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter)
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
myReceiver = MyBroadcastReceiver(mc)
LocalBroadcastManager.getInstance(applicationContext)
.registerReceiver(myReceiver!!, filter)
}
} }
// ReconnectCwtchForeground which will resync counters and settings... // ReconnectCwtchForeground which will resync counters and settings...

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.5.31'
repositories { repositories {
google() google()
// jCenter() no longer exists... https://blog.gradle.org/jcenter-shutdown // jCenter() no longer exists... https://blog.gradle.org/jcenter-shutdown

View File

@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017 #Mon Jun 20 10:33:21 PDT 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip zipStoreBase=GRADLE_USER_HOME

View File

@ -1,61 +0,0 @@
import 'package:cwtch/third_party/linkify/linkify.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(bcontext)!.clickableLinksWarning),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinksCopy, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinksCopy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.copiedToClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinkOpen, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinkOpen),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.clickableLinkError),
);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
}
},
),
),
]),
],
)),
));
});
}

View File

@ -312,6 +312,182 @@ class MaterialLocalizationLu extends MaterialLocalizations {
@override @override
String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN'; String get viewLicensesButtonLabel => 'LIZENZEN ANZEIGEN';
// ***** NEW *****
@override
String get keyboardKeyAlt => 'Alt';
@override
String get keyboardKeyAltGraph => 'AltGr';
@override
String get keyboardKeyBackspace => 'Backspace';
@override
String get keyboardKeyCapsLock => 'Caps Lock';
@override
String get keyboardKeyChannelDown => 'Kanal Erof';
@override
String get keyboardKeyChannelUp => 'Kanal Up';
@override
String get keyboardKeyControl => 'Ctrl';
@override
String get keyboardKeyDelete => 'Del';
@override
String get keyboardKeyEisu => 'Eisū';
@override
String get keyboardKeyEject => 'Eject';
@override
String get keyboardKeyEnd => 'End';
@override
String get keyboardKeyEscape => 'Esc';
@override
String get keyboardKeyFn => 'Fn';
@override
String get keyboardKeyHangulMode => 'Hangul Mode';
@override
String get keyboardKeyHanjaMode => 'Hanja Mode';
@override
String get keyboardKeyHankaku => 'Hankaku';
@override
String get keyboardKeyHiragana => 'Hiragana';
@override
String get keyboardKeyHiraganaKatakana => 'Hiragana Katakana';
@override
String get keyboardKeyHome => 'Home';
@override
String get keyboardKeyInsert => 'Insert';
@override
String get keyboardKeyKanaMode => 'Kana Mode';
@override
String get keyboardKeyKanjiMode => 'Kanji Mode';
@override
String get keyboardKeyKatakana => 'Katakana';
@override
String get keyboardKeyMeta => 'Meta';
@override
String get keyboardKeyMetaMacOs => 'Command';
@override
String get keyboardKeyMetaWindows => 'Win';
@override
String get keyboardKeyNumLock => 'Num Lock';
@override
String get keyboardKeyNumpad0 => 'Num 0';
@override
String get keyboardKeyNumpad1 => 'Num 1';
@override
String get keyboardKeyNumpad2 => 'Num 2';
@override
String get keyboardKeyNumpad3 => 'Num 3';
@override
String get keyboardKeyNumpad4 => 'Num 4';
@override
String get keyboardKeyNumpad5 => 'Num 5';
@override
String get keyboardKeyNumpad6 => 'Num 6';
@override
String get keyboardKeyNumpad7 => 'Num 7';
@override
String get keyboardKeyNumpad8 => 'Num 8';
@override
String get keyboardKeyNumpad9 => 'Num 9';
@override
String get keyboardKeyNumpadAdd => 'Num +';
@override
String get keyboardKeyNumpadComma => 'Num ,';
@override
String get keyboardKeyNumpadDecimal => 'Num .';
@override
String get keyboardKeyNumpadDivide => 'Num /';
@override
String get keyboardKeyNumpadEnter => 'Num Enter';
@override
String get keyboardKeyNumpadEqual => 'Num =';
@override
String get keyboardKeyNumpadMultiply => 'Num *';
@override
String get keyboardKeyNumpadParenLeft => 'Num (';
@override
String get keyboardKeyNumpadParenRight => 'Num )';
@override
String get keyboardKeyNumpadSubtract => 'Num -';
@override
String get keyboardKeyPageDown => 'PgDown';
@override
String get keyboardKeyPageUp => 'PgUp';
@override
String get keyboardKeyPower => 'Power';
@override
String get keyboardKeyPowerOff => 'Power Off';
@override
String get keyboardKeyPrintScreen => 'Print Screen';
@override
String get keyboardKeyRomaji => 'Romaji';
@override
String get keyboardKeyScrollLock => 'Scroll Lock';
@override
String get keyboardKeySelect => 'Select';
@override
String get keyboardKeySpace => 'Spasie';
@override
String get keyboardKeyZenkaku => 'Zenkaku';
@override
String get keyboardKeyZenkakuHankaku => 'Zenkaku Hankaku';
@override @override
String aboutListTileTitle(String applicationName) { String aboutListTileTitle(String applicationName) {
return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName); return aboutListTileTitleRaw.replaceFirst("$applicationName", applicationName);

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "cy", "@@locale": "cy",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"formattingExperiment": "Fformatio Neges", "formattingExperiment": "Fformatio Neges",
"clickableLinkOpen": "Agor URL", "clickableLinkOpen": "Agor URL",
"clickableLinksCopy": "Copïo URL", "clickableLinksCopy": "Copïo URL",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "da", "@@locale": "da",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Die Akku Optimierungen können nicht innerhalb von Cwtch wieder aktiviert werden. Bitte gehe zu Android \/ Einstellungen \/ Apps \/ Cwtch \/ Akku und setze die Akku Nutzung auf 'Optimiert'", "settingsAndroidPowerReenablePopup": "Die Akku Optimierungen können nicht innerhalb von Cwtch wieder aktiviert werden. Bitte gehe zu Android \/ Einstellungen \/ Apps \/ Cwtch \/ Akku und setze die Akku Nutzung auf 'Optimiert'",
"settingAndroidPowerExemptionDescription": "Optional: Fordere Android auf, Cwtch von der optimierten Energieverwaltung auszunehmen. Dies wird zu einer besseren Stabilität auf Kosten eines höheren Batterieverbrauchs führen.", "settingAndroidPowerExemptionDescription": "Optional: Fordere Android auf, Cwtch von der optimierten Energieverwaltung auszunehmen. Dies wird zu einer besseren Stabilität auf Kosten eines höheren Batterieverbrauchs führen.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "el", "@@locale": "el",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,15 +1,6 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"acceptGroupBtn": "Accepter",
"settingAndroidPowerExemptionDescription": "Android applique par défaut un profil de gestion de l'énergie \"optimisé\" aux applications, ce qui peut entraîner leur arrêt ou leur suppression. Demandez à Android d'exempter Cwtch de ce profil pour une meilleure stabilité mais une plus grande consommation d'énergie.", "settingAndroidPowerExemptionDescription": "Android applique par défaut un profil de gestion de l'énergie \"optimisé\" aux applications, ce qui peut entraîner leur arrêt ou leur suppression. Demandez à Android d'exempter Cwtch de ce profil pour une meilleure stabilité mais une plus grande consommation d'énergie.",
"settingsAndroidPowerReenablePopup": "Impossible de réactiver l'optimisation de la batterie à partir de Cwtch. Veuillez aller dans Android \/ Paramètres \/ Apps \/ Cwtch \/ Batterie et régler l'utilisation sur 'Optimisé'.", "settingsAndroidPowerReenablePopup": "Impossible de réactiver l'optimisation de la batterie à partir de Cwtch. Veuillez aller dans Android \/ Paramètres \/ Apps \/ Cwtch \/ Batterie et régler l'utilisation sur 'Optimisé'.",
"okButton": "OK", "okButton": "OK",
@ -21,6 +12,7 @@
"clickableLinksCopy": "Copier l'URL", "clickableLinksCopy": "Copier l'URL",
"clickableLinkOpen": "Ouvrir l'URL", "clickableLinkOpen": "Ouvrir l'URL",
"clickableLinkError": "Erreur rencontrée lors de la tentative d'ouverture de l'URL", "clickableLinkError": "Erreur rencontrée lors de la tentative d'ouverture de l'URL",
"acceptGroupBtn": "Accepter",
"successfullyImportedProfile": "Profil importé avec succès : %profile", "successfullyImportedProfile": "Profil importé avec succès : %profile",
"shuttingDownApp": "Fermeture...", "shuttingDownApp": "Fermeture...",
"importProfileTooltip": "Utilisez une sauvegarde Cwtch chiffrée pour importer un profil créé dans une autre instance de Cwtch.", "importProfileTooltip": "Utilisez une sauvegarde Cwtch chiffrée pour importer un profil créé dans une autre instance de Cwtch.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"settingsAndroidPowerReenablePopup": "Impossibile riattivare l'ottimizzazione della batteria dall'interno di Cwtch. Vai su Android \/ Impostazioni \/ Apps \/ Cwtch \/ Informazioni App \/ (Utilizzo) Batteria e imposta su 'Ottimizzato'.", "settingsAndroidPowerReenablePopup": "Impossibile riattivare l'ottimizzazione della batteria dall'interno di Cwtch. Vai su Android \/ Impostazioni \/ Apps \/ Cwtch \/ Informazioni App \/ (Utilizzo) Batteria e imposta su 'Ottimizzato'.",
"puzzleGameBtn": "Gioco di puzzle", "puzzleGameBtn": "Gioco di puzzle",
"editProfileTitle": "Modifica il profilo", "editProfileTitle": "Modifica il profilo",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "lb", "@@locale": "lb",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "no", "@@locale": "no",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "pl", "@@locale": "pl",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Nie udało się ponownie włączyć optymalizacji użycia baterii dla Cwtch. Przejdź do Android \/ Ustawienia \/ Aplikacje \/ Cwtch \/ Bateria i ustaw Zużycie na 'Optymalizacja'", "settingsAndroidPowerReenablePopup": "Nie udało się ponownie włączyć optymalizacji użycia baterii dla Cwtch. Przejdź do Android \/ Ustawienia \/ Aplikacje \/ Cwtch \/ Bateria i ustaw Zużycie na 'Optymalizacja'",
"settingAndroidPowerExemptionDescription": "Opcjonalne: wyłącz optymalizację użycia baterii przez Cwtch w systemie Android. Będzie to skutkować lepszą stabilnością w zamian za wyższy pobór energii", "settingAndroidPowerExemptionDescription": "Opcjonalne: wyłącz optymalizację użycia baterii przez Cwtch w systemie Android. Będzie to skutkować lepszą stabilnością w zamian za wyższy pobór energii",
@ -27,6 +19,12 @@
"importProfile": "Importuj profil", "importProfile": "Importuj profil",
"exportProfileTooltip": "Utwórz zaszyfrowany plik z kopią zapasową tego profilu. Zaszyfrowany plik można zaimportować do Cwtch na innym urządzeniu.", "exportProfileTooltip": "Utwórz zaszyfrowany plik z kopią zapasową tego profilu. Zaszyfrowany plik można zaimportować do Cwtch na innym urządzeniu.",
"exportProfile": "Eksportuj profil", "exportProfile": "Eksportuj profil",
"deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić",
"localeLb": "Luksemburski",
"localeNo": "Norweski",
"localeEl": "Grecki",
"localeCy": "Walijski",
"localeDa": "Duński",
"localeRo": "Romanian", "localeRo": "Romanian",
"newMessageNotificationConversationInfo": "Nowa wiadomość od %1", "newMessageNotificationConversationInfo": "Nowa wiadomość od %1",
"newMessageNotificationSimple": "Nowa wiadomość", "newMessageNotificationSimple": "Nowa wiadomość",
@ -64,23 +62,6 @@
"torSettingsCustomSocksPort": "Port SOCKS", "torSettingsCustomSocksPort": "Port SOCKS",
"torSettingsEnabledAdvancedDescription": "Użyj obecnego na twoim systemie serwisu Tor lub zmień parametry serwisu Tor w Cwtch", "torSettingsEnabledAdvancedDescription": "Użyj obecnego na twoim systemie serwisu Tor lub zmień parametry serwisu Tor w Cwtch",
"torSettingsEnabledAdvanced": "Włącz zaawansowaną konfigurację Tor", "torSettingsEnabledAdvanced": "Włącz zaawansowaną konfigurację Tor",
"archiveConversation": "Zarchiwizuj tę konwersację",
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Grupy (eksperymentalne) w Ustawieniach",
"descriptionExperimentsGroups": "Grupy (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.",
"descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. Grupy, integracja z botami, itp.",
"invalidImportString": "Niepoprawny ciąg importu",
"enableGroups": "Włącz Grupy",
"todoPlaceholder": "Do zrobienia...",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"copiedToClipboardNotification": "Skopiowano do schowka",
"searchList": "Lista wyszukiwania",
"serverLabel": "Server",
"deleteConfirmLabel": "Wpisz USUŃ aby potwierdzić",
"localeLb": "Luksemburski",
"localeNo": "Norweski",
"localeEl": "Grecki",
"localeCy": "Walijski",
"localeDa": "Duński",
"largeTextLabel": "Duży", "largeTextLabel": "Duży",
"settingInterfaceZoom": "Przybliżenie", "settingInterfaceZoom": "Przybliżenie",
"localeDe": "Deutsche", "localeDe": "Deutsche",
@ -111,6 +92,7 @@
"password1Label": "Hasło", "password1Label": "Hasło",
"currentPasswordLabel": "Obecne hasło", "currentPasswordLabel": "Obecne hasło",
"yourDisplayName": "Nazwa", "yourDisplayName": "Nazwa",
"profileOnionLabel": "Przekaż ten adres osobom, z którymi chcesz nawiązać kontakt",
"noPasswordWarning": "Brak hasła do konta oznacza, że dane przechowywane na tym urządzeniu nie będą zaszyfrowane", "noPasswordWarning": "Brak hasła do konta oznacza, że dane przechowywane na tym urządzeniu nie będą zaszyfrowane",
"radioNoPassword": "Niezaszyfrowany (brak hasła)", "radioNoPassword": "Niezaszyfrowany (brak hasła)",
"radioUsePassword": "Hasło", "radioUsePassword": "Hasło",
@ -120,13 +102,13 @@
"profileName": "Nazwa", "profileName": "Nazwa",
"editProfileTitle": "Edytuj profil", "editProfileTitle": "Edytuj profil",
"addProfileTitle": "Dodaj nowy profil", "addProfileTitle": "Dodaj nowy profil",
"deleteBtn": "Delete", "deleteBtn": "Usuń",
"unblockBtn": "Odblokuj", "unblockBtn": "Odblokuj",
"dontSavePeerHistory": "Nie", "dontSavePeerHistory": "Nie",
"savePeerHistoryDescription": "Zapisywanie wiadomości", "savePeerHistoryDescription": "Zapisywanie wiadomości",
"savePeerHistory": "Tak", "savePeerHistory": "Tak",
"blockBtn": "Zablokuj", "blockBtn": "Zablokuj",
"saveBtn": "Save", "saveBtn": "Zapisz",
"displayNameLabel": "Nazwa", "displayNameLabel": "Nazwa",
"addressLabel": "Adresy", "addressLabel": "Adresy",
"puzzleGameBtn": "Puzzle", "puzzleGameBtn": "Puzzle",
@ -147,6 +129,7 @@
"membershipDescription": "Lista użytkowników, którzy wysyłali wiadomości w tej grupie. Członkowie grupy, którzy nie wysyłali żadnych wiadomości nie są na tej liście.", "membershipDescription": "Lista użytkowników, którzy wysyłali wiadomości w tej grupie. Członkowie grupy, którzy nie wysyłali żadnych wiadomości nie są na tej liście.",
"addListItemBtn": "Dodaj", "addListItemBtn": "Dodaj",
"peerNotOnline": "Znajomy jest niedostępny. Nie można użyć aplikacji.", "peerNotOnline": "Znajomy jest niedostępny. Nie można użyć aplikacji.",
"searchList": "Lista wyszukiwania",
"update": "Zaktualizuj", "update": "Zaktualizuj",
"inviteBtn": "Zaproś", "inviteBtn": "Zaproś",
"inviteToGroupLabel": "Zaproś do grupy", "inviteToGroupLabel": "Zaproś do grupy",
@ -158,6 +141,7 @@
"serverConnectivityConnected": "Połączono z serwerem", "serverConnectivityConnected": "Połączono z serwerem",
"serverInfo": "Informacje o serwerze", "serverInfo": "Informacje o serwerze",
"invitationLabel": "Zaproszenie", "invitationLabel": "Zaproszenie",
"serverLabel": "Server",
"search": "Szukaj...", "search": "Szukaj...",
"blocked": "Zablokowany", "blocked": "Zablokowany",
"pasteAddressToAddContact": "Wklej adres Cwtch znajomego, zaproszenie do grupy albo pęk kluczy", "pasteAddressToAddContact": "Wklej adres Cwtch znajomego, zaproszenie do grupy albo pęk kluczy",
@ -241,6 +225,7 @@
"tooltipRejectContactRequest": "Odrzuć zaproszenie do znajomych", "tooltipRejectContactRequest": "Odrzuć zaproszenie do znajomych",
"tooltipAcceptContactRequest": "Akceptuj zaproszenie do znajomych", "tooltipAcceptContactRequest": "Akceptuj zaproszenie do znajomych",
"notificationNewMessageFromPeer": "Nowa wiadomość od znajomego!", "notificationNewMessageFromPeer": "Nowa wiadomość od znajomego!",
"groupInviteSettingsWarning": "Zaproszono Cię do grupy! Aby wyświetlić to zaproszenie, włącz Grupy (eksperymentalne) w Ustawieniach",
"shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.", "shutdownCwtchDialog": "Zamknąć Cwtch? Wszystkie połączenia zostaną zakończone, a aplikacja zostanie zamknięta.",
"malformedMessage": "Wiadomość uszkodzona", "malformedMessage": "Wiadomość uszkodzona",
"profileDeleteSuccess": "Profil został usunięty", "profileDeleteSuccess": "Profil został usunięty",
@ -263,16 +248,21 @@
"inviteToGroup": "Zaproszono Cię do grupy:", "inviteToGroup": "Zaproszono Cię do grupy:",
"successfullAddedContact": "Dodano znajomego ", "successfullAddedContact": "Dodano znajomego ",
"descriptionBlockUnknownConnections": "Blokowanie połączeń od osób, które nie są na liście Twoich znajomych.", "descriptionBlockUnknownConnections": "Blokowanie połączeń od osób, które nie są na liście Twoich znajomych.",
"descriptionExperimentsGroups": "Grupy (eksperymentalne) łączą się z niezaufanymi serwerami, aby umożliwić komunikację grupową.",
"descriptionExperiments": "Funkcje eksperymentalne są opcjonalne. Dodają one funkcjonalności, które mogą być mniej prywatne niż domyślne konwersacje 1:1, np. Grupy, integracja z botami, itp.",
"titleManageProfiles": "Zarządzaj Profilami", "titleManageProfiles": "Zarządzaj Profilami",
"tooltipUnlockProfiles": "Wprowadź hasło, aby odblokować zaszyfrowane profile.", "tooltipUnlockProfiles": "Wprowadź hasło, aby odblokować zaszyfrowane profile.",
"titleManageContacts": "Konwersacje", "titleManageContacts": "Konwersacje",
"tooltipAddContact": "Dodaj znajomego lub grupę", "tooltipAddContact": "Dodaj znajomego lub grupę",
"tooltipOpenSettings": "Ustawienia", "tooltipOpenSettings": "Ustawienia",
"contactAlreadyExists": "Ten znajomy już istnieje", "contactAlreadyExists": "Ten znajomy już istnieje",
"invalidImportString": "Niepoprawny ciąg importu",
"conversationSettings": "Ustawienia konwersacji", "conversationSettings": "Ustawienia konwersacji",
"enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.", "enterCurrentPasswordForDelete": "Aby usunąć ten profil, wprowadź hasło.",
"enableGroups": "Włącz Grupy",
"localeIt": "Italiana", "localeIt": "Italiana",
"localeEs": "Espanol", "localeEs": "Espanol",
"todoPlaceholder": "Do zrobienia...",
"addNewItem": "Dodaj do listy", "addNewItem": "Dodaj do listy",
"addListItem": "Add a New List Item", "addListItem": "Add a New List Item",
"newConnectionPaneTitle": "Nowe połączenie", "newConnectionPaneTitle": "Nowe połączenie",
@ -331,6 +321,7 @@
"fileSavedTo": "Zapisano do", "fileSavedTo": "Zapisano do",
"verfiyResumeButton": "Zweryfikuj\/wznów", "verfiyResumeButton": "Zweryfikuj\/wznów",
"copyServerKeys": "Kopiuj klucze", "copyServerKeys": "Kopiuj klucze",
"archiveConversation": "Zarchiwizuj tę konwersację",
"streamerModeLabel": "Tryb streamera\/prezentacji", "streamerModeLabel": "Tryb streamera\/prezentacji",
"retrievingManifestMessage": "Pobieranie informacji o pliku...", "retrievingManifestMessage": "Pobieranie informacji o pliku...",
"openFolderButton": "Otwórz folder", "openFolderButton": "Otwórz folder",
@ -339,5 +330,6 @@
"labelFilesize": "Rozmiar", "labelFilesize": "Rozmiar",
"messageFileSent": "Plik został wysłany", "messageFileSent": "Plik został wysłany",
"tooltipSendFile": "Wyślij plik", "tooltipSendFile": "Wyślij plik",
"settingFileSharing": "Udostępnianie plików" "settingFileSharing": "Udostępnianie plików",
"copiedToClipboardNotification": "Skopiowano do schowka"
} }

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,14 +1,6 @@
{ {
"@@locale": "ro", "@@locale": "ro",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",

View File

@ -1,84 +1,24 @@
{ {
"@@locale": "ru", "@@locale": "ru",
"@@last_modified": "2022-06-16T18:20:12+02:00", "@@last_modified": "2022-04-21T21:35:58+02:00",
"tooltipPreviewFormatting": "Preview Message Formatting",
"tooltipCode": "Code \/ Monospace",
"tooltipStrikethrough": "Strikethrough",
"tooltipSubscript": "Subscript",
"tooltipSuperscript": "Superscript",
"tooltipItalicize": "Italic",
"tooltipBackToMessageEditing": "Back to Message Editing",
"tooltipBoldText": "Bold",
"editProfile": "Изменить профиль",
"okButton": "OK", "okButton": "OK",
"settingsAndroidPowerReenablePopup": "Невозможно перезапустить функцию оптимазации батарее для Cwtch. Перейдите в настройки Android \/ Настройки \/ Приложения и уведомления \/ Все приложения \/ Cwtch \/ Батарея \/ Эконоимя заряда \/ Отключена", "settingsAndroidPowerReenablePopup": "Cannot re-enable Battery Optimization from within Cwtch. Please go to Android \/ Settings \/ Apps \/ Cwtch \/ Battery and set Usage to 'Optimized'",
"settingAndroidPowerExemptionDescription": "Необязательно: в настройках Android исключите Cwtch в параметрах оптимизации батареи. Это улучшит стабильность за счёт небольшого расхода батареи..", "settingAndroidPowerExemptionDescription": "Optional: Request Android to exempt Cwtch from optimized power management. This will result in better stability at the cost of greater battery use.",
"settingAndroidPowerExemption": "Игнорировать оптимазацию батареи Android", "settingAndroidPowerExemption": "Android Ignore Battery Optimizations",
"thisFeatureRequiresGroupExpermientsToBeEnabled": "Чтобы использовать данную функцию, в настройках необходимо включить \"Эксперементы\", затем \"Групповые чаты\"", "thisFeatureRequiresGroupExpermientsToBeEnabled": "This feature requires the Groups Experiment to be enabled in Settings",
"messageFormattingDescription": "Включите форматирование, если к примеру хотите использовать **жирный-текст** и *курсив*", "messageFormattingDescription": "Enable rich text formatting in displayed messages e.g. **bold** and *italic*",
"formattingExperiment": "Форматирование сообщений", "formattingExperiment": "Message Formatting",
"clickableLinkError": "Ошибка при попытке открыть данную ссылку", "clickableLinkError": "Error encountered while attempting to open URL",
"clickableLinksCopy": "Копировать ссылку", "clickableLinksCopy": "Copy URL",
"clickableLinkOpen": "Открыть ссылку", "clickableLinkOpen": "Open URL",
"clickableLinksWarning": "Открытие данной ссылки приведет к запуску приложения за пределами Cwtch и может раскрыть метаданные или иным образом поставить под угрозу безопасность Cwtch. Открывайте ссылки только от тех людей, которым вы доверяете. Вы уверены, что хотите продолжить?", "clickableLinksWarning": "Opening this URL will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open URLs from people you trust. Are you sure you want to continue?",
"shuttingDownApp": "Выключение...", "shuttingDownApp": "Shutting down...",
"successfullyImportedProfile": "Профиль успешно импортирован: %profile", "successfullyImportedProfile": "Successfully Imported Profile: %profile",
"failedToImportProfile": "Ошибка импорта профиля", "failedToImportProfile": "Error Importing Profile",
"importProfileTooltip": "Используйте зашифрованную резервную копию Cwtch, чтобы перенести профиль, созданный на другом устройстве где установлен Cwtch..", "importProfileTooltip": "Use an encrypted Cwtch backup to bring in a profile created in another instance of Cwtch.",
"importProfile": "Загрузить профиль", "importProfile": "Import Profile",
"exportProfileTooltip": "Сделать зашифрованную резервную копию в файл. Его потом потом можно импортировать на других устройствах где установлен Cwtch.", "exportProfileTooltip": "Backup this profile to an encrypted file. The encrypted file can be imported into another Cwtch app.",
"exportProfile": "Сохранить профиль", "exportProfile": "Export Profile",
"notificationContentContactInfo": "Информация о разговоре",
"notificationContentSimpleEvent": "Обычное событие",
"conversationNotificationPolicySettingDescription": "Настройка уведомлений",
"conversationNotificationPolicySettingLabel": "Настройка уведомлений",
"settingsGroupAppearance": "Настройки отображения",
"notificationContentSettingDescription": "Управление уведомлениями в данной теме",
"notificationPolicySettingDescription": "Настройка уведомлений по-умолчанию",
"notificationContentSettingLabel": "Содержимое уведомления",
"notificationPolicySettingLabel": "Уведомления",
"conversationNotificationPolicyOptIn": "Включить",
"conversationNotificationPolicyDefault": "По-умолчанию",
"notificationPolicyDefaultAll": "Всё по-умолчанию",
"notificationPolicyOptIn": "Включить",
"notificationPolicyMute": "Без звука",
"tooltipSelectACustomProfileImage": "Сменить изображение профиля",
"torSettingsEnabledCacheDescription": "Кэшировать текущий загруженный узел Tor для повторного подключения при следующем запуске Cwtch. Это позволит Tor запускаться быстрее. Если этот параметр отключен, Cwtch будет очищать кэшированные данные при запуске.",
"torSettingsEnableCache": "Кешировать узлы Tor",
"descriptionACNCircuitInfo": "Подробная информация о соединении, который сеть анонимной связи использует для подключения к этому разговору.",
"labelACNCircuitInfo": "Информация о цепи ACN",
"fileSharingSettingsDownloadFolderTooltip": "Нажмите обзор чтобы выбрать другую папку по-умолчанию для загружаемых файлов.",
"fileSharingSettingsDownloadFolderDescription": "При включение функции автоматическое скачивание файлов (например картинок), необходимо указать папку для сохранения.",
"torSettingsErrorSettingPort": "Номер порта должен быть в диапазоне от 1 до 65535",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Переопределение конфигурации Tor по умолчанию. Предупреждение: это может быть опасно. Если не знаете, что делаете, лучше не трогать!",
"torSettingsUseCustomTorServiceConfiguration": "Используйте пользовательскую конфигурацию службы Tor (torrc)",
"torSettingsCustomControlPortDescription": "Используйте настраиваемый порт для управления подключениями к прокси-серверу Tor.",
"torSettingsCustomControlPort": "Выберите контрольный порт",
"torSettingsCustomSocksPortDescription": "Используйте настраиваемый порт для подключения к прокси-серверу Tor.",
"torSettingsCustomSocksPort": "Выберите SOCKS порт",
"torSettingsEnabledAdvancedDescription": "Использовать установленную службу Tor в вашей системе или измените параметры службы Cwtch Tor",
"torSettingsEnabledAdvanced": "Включить расширенные настройки Tor",
"themeColorLabel": "Основной цвет темы",
"settingDownloadFolder": "Папка для загрузок",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами.",
"importLocalServerLabel": "Использовать локальный сервер",
"deleteServerConfirmBtn": "Вы точно хотите удалить сервер?",
"unlockProfileTip": "Создайте или импортируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или импортируйте сервер, чтобы начать",
"saveServerButton": "Сохранить",
"serverEnabled": "Состояние сервера",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch, поверх скрытой сети Tor",
"settingUIColumnOptionSame": "Как в портретном режиме",
"settingUIColumnSingle": "Один столбец",
"createProfileToBegin": "Пожалуйста, создайте или импортируйте профиль, чтобы начать",
"yesLeave": "Удалить",
"leaveConversation": "Удалить",
"enableGroups": "Групповые чаты",
"settingTheme": "Ночной режим",
"addNewProfileBtn": "Создать новый профиль",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistoryDescription": "Определяет политику хранения или удаления сообщений с данным контактом",
"savePeerHistory": "Настройка истории",
"deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить", "deleteConfirmLabel": "Введите УДАЛИТЬ, чтобы продолжить",
"deleteConfirmText": "УДАЛИТЬ", "deleteConfirmText": "УДАЛИТЬ",
"localeCy": "Валлийский", "localeCy": "Валлийский",
@ -86,24 +26,49 @@
"localeEl": "Греческий", "localeEl": "Греческий",
"localeNo": "Норвежский", "localeNo": "Норвежский",
"localeLb": "Люксембургский", "localeLb": "Люксембургский",
"settingsGroupAppearance": "Появление",
"settingGroupBehaviour": "Поведение", "settingGroupBehaviour": "Поведение",
"settingsGroupExperiments": "Эксперименты", "settingsGroupExperiments": "Эксперименты",
"labelTorNetwork": "Сеть Tor", "labelTorNetwork": "Сеть Tor",
"notificationPolicyMute": "Тишина",
"conversationNotificationPolicyNever": "Никогда", "conversationNotificationPolicyNever": "Никогда",
"newMessageNotificationConversationInfo": "Новое сообщение от %1", "newMessageNotificationConversationInfo": "Новое сообщение от %1",
"newMessageNotificationSimple": "Новое сообщение", "newMessageNotificationSimple": "Новое сообщение",
"localeRo": "Румынский", "localeRo": "Румынский",
"notificationContentContactInfo": "Conversation Information",
"notificationContentSimpleEvent": "Plain Event",
"conversationNotificationPolicySettingDescription": "Control notification behaviour for this conversation",
"conversationNotificationPolicySettingLabel": "Conversation Notification Policy",
"notificationContentSettingDescription": "Controls the contents of conversation notifications",
"notificationPolicySettingDescription": "Controls the default application notification behaviour",
"notificationContentSettingLabel": "Notification Content",
"notificationPolicySettingLabel": "Notification Policy",
"conversationNotificationPolicyOptIn": "Opt In",
"conversationNotificationPolicyDefault": "Default",
"notificationPolicyDefaultAll": "Default All",
"notificationPolicyOptIn": "Opt In",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"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",
"descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.",
"labelACNCircuitInfo": "ACN Circuit Info",
"fileSharingSettingsDownloadFolderTooltip": "Browse to select a different default folder for downloaded files.",
"fileSharingSettingsDownloadFolderDescription": "When files are downloaded automatically (e.g. image files, when image previews are enabled) a default location to download the files to is needed.",
"torSettingsErrorSettingPort": "Port Number must be between 1 and 65535",
"msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.", "msgAddToAccept": "Добавьте учетную запись в контакты, чтобы принять этот файл.",
"btnSendFile": "Отправить файл", "btnSendFile": "Отправить файл",
"msgConfirmSend": "Вы уверены, что хотите отправить?", "msgConfirmSend": "Вы уверены, что хотите отправить?",
"msgFileTooBig": "Размер файла не должен превышать 10GB", "msgFileTooBig": "Размер файла не должен превышать 10GB",
"storageMigrationModalMessage": "Перенос профилей в новый формат хранения. Это может занять несколько минут...", "storageMigrationModalMessage": "Перенос профилей в новый формат хранения. Это может занять несколько минут...",
"loadingCwtch": "Загрузка Cwtch...", "loadingCwtch": "Загрузка Cwtch...",
"themeColorLabel": "Светлая или Темная тема",
"settingDownloadFolder": "Папка для скачивания",
"serverConnectionsLabel": "Всего соединений:", "serverConnectionsLabel": "Всего соединений:",
"serverTotalMessagesLabel": "Всего сообщений:", "serverTotalMessagesLabel": "Всего сообщений:",
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой Onion сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.", "plainServerDescription": "Мы настоятельно рекомендуем защитить свой Onion сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
"settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер для Cwtch в скрытой сети Tor. В меню появится дополнительная опция Серверы", "settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер для Cwtch в скрытой сети Tor. В меню появится дополнительная опция Серверы",
"streamerModeLabel": "Режим маскировки", "streamerModeLabel": "Режим маскировки",
"settingUIColumnSingle": "Один стобец",
"settingUIColumnLandscape": "Столбцы чатов в ландшафтном режиме", "settingUIColumnLandscape": "Столбцы чатов в ландшафтном режиме",
"settingUIColumnPortrait": "Столбцы чатов в портретном режиме", "settingUIColumnPortrait": "Столбцы чатов в портретном режиме",
"resetTor": "Сброс", "resetTor": "Сброс",
@ -112,6 +77,14 @@
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..", "descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный чат 1 на 1..",
"settingLanguage": "Выбрать язык", "settingLanguage": "Выбрать язык",
"profileName": "Введите имя...", "profileName": "Введите имя...",
"torSettingsUseCustomTorServiceConfigurastionDescription": "Override the default tor configuration. Warning: This could be dangerous. Only turn this on if you know what you are doing.",
"torSettingsUseCustomTorServiceConfiguration": "Use a Custom Tor Service Configuration (torrc)",
"torSettingsCustomControlPortDescription": "Use a custom port for control connections to the Tor proxy",
"torSettingsCustomControlPort": "Custom Control Port",
"torSettingsCustomSocksPortDescription": "Use a custom port for data connections to the Tor proxy",
"torSettingsCustomSocksPort": "Custom SOCKS Port",
"torSettingsEnabledAdvancedDescription": "Use an existing Tor service on your system, or change the parameters of the Cwtch Tor Service",
"torSettingsEnabledAdvanced": "Enable Advanced Tor Configuration",
"themeNameNeon2": "Неон2", "themeNameNeon2": "Неон2",
"themeNameNeon1": "Неон1", "themeNameNeon1": "Неон1",
"themeNameMidnight": "Полночь", "themeNameMidnight": "Полночь",
@ -121,6 +94,7 @@
"themeNameVampire": "Вампир", "themeNameVampire": "Вампир",
"themeNameWitch": "Ведьма", "themeNameWitch": "Ведьма",
"themeNameCwtch": "Cwtch", "themeNameCwtch": "Cwtch",
"settingImagePreviewsDescription": "Автоматическая загрузка изображений. Обратите внимание, что предварительный просмотр изображений часто может использоваться для взлома или деаномизации. Не используйте данную функцию если Вы контактируете с ненадежными контактами. Аватары профиля запланированы для версии Cwtch 1.6.",
"settingImagePreviews": "Предпросмотр изображений и фотографий профиля", "settingImagePreviews": "Предпросмотр изображений и фотографий профиля",
"experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях", "experimentClickableLinksDescription": "Экспериментальная функция которая позволяет нажимать на URL адреса в сообщениях",
"enableExperimentClickableLinks": "Включить кликабельные ссылки", "enableExperimentClickableLinks": "Включить кликабельные ссылки",
@ -133,8 +107,11 @@
"groupsOnThisServerLabel": "Группы, в которых я нахожусь, размещены на этом сервере", "groupsOnThisServerLabel": "Группы, в которых я нахожусь, размещены на этом сервере",
"importLocalServerButton": "Импорт %1", "importLocalServerButton": "Импорт %1",
"importLocalServerSelectText": "Выбрать локальный сервер", "importLocalServerSelectText": "Выбрать локальный сервер",
"importLocalServerLabel": "Импортировать локальный сервер",
"newMessagesLabel": "Новое сообщение", "newMessagesLabel": "Новое сообщение",
"localeRU": "Русский", "localeRU": "Русский",
"profileOnionLabel": "Send this address to contacts you want to connect with",
"savePeerHistory": "Хранить историю",
"saveBtn": "Сохранить", "saveBtn": "Сохранить",
"networkStatusOnline": "В сети", "networkStatusOnline": "В сети",
"defaultProfileName": "Алиса", "defaultProfileName": "Алиса",
@ -150,23 +127,29 @@
"fileInterrupted": "Прервано", "fileInterrupted": "Прервано",
"fileSavedTo": "Сохранить в", "fileSavedTo": "Сохранить в",
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.", "encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
"deleteServerConfirmBtn": "Точно удалить сервер?",
"deleteServerSuccess": "Сервер успешно удален", "deleteServerSuccess": "Сервер успешно удален",
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его", "enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
"copyAddress": "Копировать адрес", "copyAddress": "Копировать адрес",
"settingServers": "Использовать серверы", "settingServers": "Использовать серверы",
"enterServerPassword": "Введите пароль для разблокировки сервера", "enterServerPassword": "Введите пароль для разблокировки сервера",
"unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать",
"unlockServerTip": "Создайте или разблокируйте сервер, чтобы начать",
"addServerTooltip": "Добавить сервер", "addServerTooltip": "Добавить сервер",
"serversManagerTitleShort": "Серверы", "serversManagerTitleShort": "Серверы",
"serversManagerTitleLong": "Личные серверы", "serversManagerTitleLong": "Личные серверы",
"saveServerButton": "Сохранить сервер",
"serverAutostartDescription": "Автозапуск сервера при старте программы", "serverAutostartDescription": "Автозапуск сервера при старте программы",
"serverAutostartLabel": "Автозапуск", "serverAutostartLabel": "Автозапуск",
"serverEnabledDescription": "Запустить или остановить сервер", "serverEnabledDescription": "Запустить или остановить сервер",
"serverEnabled": "Сервер запущен",
"serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства", "serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства",
"serverDescriptionLabel": "Описание сервера", "serverDescriptionLabel": "Описание сервера",
"serverAddress": "Адрес сервера", "serverAddress": "Адрес сервера",
"editServerTitle": "Изменить сервер", "editServerTitle": "Изменить сервер",
"addServerTitle": "Добавить сервер", "addServerTitle": "Добавить сервер",
"titleManageProfilesShort": "Профили", "titleManageProfilesShort": "Профили",
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
"settingFileSharing": "Передача файлов", "settingFileSharing": "Передача файлов",
"tooltipSendFile": "Отправить файл", "tooltipSendFile": "Отправить файл",
"messageFileOffered": "Контакт предлагает загрузить вам файл", "messageFileOffered": "Контакт предлагает загрузить вам файл",
@ -188,6 +171,7 @@
"addContactConfirm": "Добавить контакт %1", "addContactConfirm": "Добавить контакт %1",
"addContact": "Добавить контакт", "addContact": "Добавить контакт",
"contactGoto": "Перейти к сообщению от %1", "contactGoto": "Перейти к сообщению от %1",
"settingUIColumnOptionSame": "Как в настройках портретного режима",
"settingUIColumnDouble14Ratio": "Двойной (1:4)", "settingUIColumnDouble14Ratio": "Двойной (1:4)",
"settingUIColumnDouble12Ratio": "Двойной (1:2)", "settingUIColumnDouble12Ratio": "Двойной (1:2)",
"localePl": "Польский", "localePl": "Польский",
@ -209,6 +193,7 @@
"debugLog": "Влючить отладку через консоль", "debugLog": "Влючить отладку через консоль",
"torNetworkStatus": "Статус сети Tor", "torNetworkStatus": "Статус сети Tor",
"addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.", "addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.",
"createProfileToBegin": "Пожалуйста, создайте или разблокируйте профиль, чтобы начать",
"nickChangeSuccess": "Имя профиля успешно изменено", "nickChangeSuccess": "Имя профиля успешно изменено",
"addServerFirst": "Перед созданием группы, необходимо создать сервер", "addServerFirst": "Перед созданием группы, необходимо создать сервер",
"deleteProfileSuccess": "Профиль успешно удален", "deleteProfileSuccess": "Профиль успешно удален",
@ -223,7 +208,9 @@
"accepted": "Принять!", "accepted": "Принять!",
"chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..", "chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"yesLeave": "Да, оставить этот чат",
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.", "reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveConversation": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:", "inviteToGroup": "Вас пригласили присоединиться к группе:",
"titleManageServers": "Управление серверами", "titleManageServers": "Управление серверами",
"successfullAddedContact": "Успешно добавлен", "successfullAddedContact": "Успешно добавлен",
@ -236,6 +223,7 @@
"invalidImportString": "Недействительная строка импорта", "invalidImportString": "Недействительная строка импорта",
"conversationSettings": "Настройки чата", "conversationSettings": "Настройки чата",
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.", "enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
"enableGroups": "Включить Групповые чаты",
"localeIt": "Итальянский", "localeIt": "Итальянский",
"localeEs": "Испанский", "localeEs": "Испанский",
"todoPlaceholder": "Выполняю...", "todoPlaceholder": "Выполняю...",
@ -255,6 +243,7 @@
"experimentsEnabled": "Включить Экспериментальные функции", "experimentsEnabled": "Включить Экспериментальные функции",
"themeDark": "Темная", "themeDark": "Темная",
"themeLight": "Светлая", "themeLight": "Светлая",
"settingTheme": "Тема",
"largeTextLabel": "Большой", "largeTextLabel": "Большой",
"settingInterfaceZoom": "Уровень масштабирования", "settingInterfaceZoom": "Уровень масштабирования",
"localeDe": "Немецкий", "localeDe": "Немецкий",
@ -271,6 +260,7 @@
"error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем", "error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем",
"password": "Пароль", "password": "Пароль",
"enterProfilePassword": "Введите пароль для просмотра ваших профилей", "enterProfilePassword": "Введите пароль для просмотра ваших профилей",
"addNewProfileBtn": "Добавить новый профиль",
"deleteProfileConfirmBtn": "Действительно удалить профиль?", "deleteProfileConfirmBtn": "Действительно удалить профиль?",
"deleteProfileBtn": "Удалить профиль", "deleteProfileBtn": "Удалить профиль",
"passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен", "passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен",
@ -285,11 +275,13 @@
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы", "noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
"radioNoPassword": "Незашифрованный (без пароля)", "radioNoPassword": "Незашифрованный (без пароля)",
"radioUsePassword": "Пароль", "radioUsePassword": "Пароль",
"editProfile": "Изменить профиль",
"newProfile": "Новый профиль", "newProfile": "Новый профиль",
"editProfileTitle": "Изменить профиль", "editProfileTitle": "Изменить профиль",
"addProfileTitle": "Добавить новый профиль", "addProfileTitle": "Добавить новый профиль",
"unblockBtn": "Разблокировать контакт", "unblockBtn": "Разблокировать контакт",
"dontSavePeerHistory": "Удалить историю", "dontSavePeerHistory": "Удалить историю",
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
"blockBtn": "Заблокировать контакт", "blockBtn": "Заблокировать контакт",
"displayNameLabel": "Отображаемое имя", "displayNameLabel": "Отображаемое имя",
"addressLabel": "Адрес", "addressLabel": "Адрес",

View File

@ -1,7 +1,6 @@
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'message.dart'; import 'message.dart';
import 'messagecache.dart'; import 'messagecache.dart';
@ -45,7 +44,6 @@ class ContactInfoState extends ChangeNotifier {
late Map<String, GlobalKey<MessageRowState>> keys; late Map<String, GlobalKey<MessageRowState>> keys;
int _newMarkerMsgIndex = -1; int _newMarkerMsgIndex = -1;
late MessageCache messageCache; late MessageCache messageCache;
ItemScrollController messageScrollController = new ItemScrollController();
// todo: a nicer way to model contacts, groups and other "entities" // todo: a nicer way to model contacts, groups and other "entities"
late bool _isGroup; late bool _isGroup;

View File

@ -21,7 +21,7 @@ class ContactListState extends ChangeNotifier {
} }
List<ContactInfoState> filteredList() { List<ContactInfoState> filteredList() {
if (!isFiltered) return _contacts; if (!isFiltered) return contacts;
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
} }

View File

@ -64,8 +64,6 @@ class FileMessage extends Message {
} }
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,
width: 50,
height: 50,
child: FileBubble( child: FileBubble(
nameSuggestion, nameSuggestion,
rootHash, rootHash,
@ -73,7 +71,6 @@ class FileMessage extends Message {
fileSize, fileSize,
isAuto: metadata.isAuto, isAuto: metadata.isAuto,
interactive: false, interactive: false,
isPreview: true,
)); ));
}); });
} }

View File

@ -1,12 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messagerow.dart'; import 'package:cwtch/widgets/messagerow.dart';
import 'package:cwtch/widgets/quotedmessage.dart'; import 'package:cwtch/widgets/quotedmessage.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../main.dart';
import '../messagecache.dart';
import '../profile.dart';
class QuotedMessageStructure { class QuotedMessageStructure {
final String quotedHash; final String quotedHash;
final String body; final String body;
@ -29,14 +34,8 @@ class QuotedMessage extends Message {
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
try { try {
dynamic message = jsonDecode( dynamic message = jsonDecode(this.content);
this.content, return Text(message["body"]);
);
var content = message["body"];
return Text(
content,
overflow: TextOverflow.ellipsis,
);
} catch (e) { } catch (e) {
return MalformedBubble(); return MalformedBubble();
} }

View File

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messages/malformedmessage.dart'; import 'package:cwtch/models/messages/malformedmessage.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
@ -21,10 +19,7 @@ class TextMessage extends Message {
return ChangeNotifierProvider.value( return ChangeNotifierProvider.value(
value: this.metadata, value: this.metadata,
builder: (bcontext, child) { builder: (bcontext, child) {
return Text( return SelectableText(this.content);
this.content,
overflow: TextOverflow.ellipsis,
);
}); });
} }

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:cwtch/models/remoteserver.dart'; import 'package:cwtch/models/remoteserver.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'contact.dart'; import 'contact.dart';
import 'contactlist.dart'; import 'contactlist.dart';
@ -21,7 +20,7 @@ class ProfileInfoState extends ChangeNotifier {
bool _online = false; bool _online = false;
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>(); Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
Map<String, int> _downloadTriggers = Map<String, int>(); Map<String, int> _downloadTriggers = Map<String, int>();
ItemScrollController contactListScrollController = new ItemScrollController();
// assume profiles are encrypted...this will be set to false // assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password. // in the constructor if the profile is encrypted with the defacto password.
bool _encrypted = true; bool _encrypted = true;

View File

@ -10,6 +10,8 @@ import 'package:desktop_notifications/desktop_notifications.dart' as linux_notif
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart'; import 'package:flutter_local_notifications_linux/flutter_local_notifications_linux.dart';
import 'package:flutter_local_notifications_linux/src/model/hint.dart'; import 'package:flutter_local_notifications_linux/src/model/hint.dart';
import 'package:flutter_local_notifications_linux/src/model/icon.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@ -55,22 +57,33 @@ class WindowsNotificationManager implements NotificationsManager {
} }
} }
// LinuxNotificationsManager uses the desktop_notifications package to implement class NotificationPayload {
// the standard dbus-powered linux desktop notifications. late String profileOnion;
class LinuxNotificationsManager implements NotificationsManager { late int convoId;
int previous_id = 0;
late linux_notifications.NotificationsClient client;
late Future<void> Function(String, int) notificationSelectConvo;
late String assetsPath;
LinuxNotificationsManager(Future<void> Function(String, int) notificationSelectConvo) { NotificationPayload(String po, int cid) {
this.client = linux_notifications.NotificationsClient(); profileOnion = po;
this.notificationSelectConvo = notificationSelectConvo; convoId = cid;
scheduleMicrotask(() async {
assetsPath = await detectLinuxAssetsPath();
});
} }
NotificationPayload.fromJson(Map<String, dynamic> json)
: profileOnion = json['profileOnion'],
convoId = json['convoId'];
Map<String, dynamic> toJson() => {
'profileOnion': profileOnion,
'convoId': convoId,
};
}
// FlutterLocalNotificationsPlugin based NotificationManager that handles MacOS and Linux
// TODO: Windows support is being worked on, check back and migrate to that too when it lands
class NixNotificationManager implements NotificationsManager {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
late Future<void> Function(String, int) notificationSelectConvo;
late String linuxAssetsPath;
// Cwtch can install in non flutter supported ways on linux, this code detects where the assets are on Linux // Cwtch can install in non flutter supported ways on linux, this code detects where the assets are on Linux
Future<String> detectLinuxAssetsPath() async { Future<String> detectLinuxAssetsPath() async {
var devStat = FileStat.stat("assets"); var devStat = FileStat.stat("assets");
@ -90,53 +103,26 @@ class LinuxNotificationsManager implements NotificationsManager {
return ""; return "";
} }
Future<void> notify(String message, String profile, int conversationId) async {
var iconPath = Uri.file(path.join(assetsPath, "assets/knott.png"));
client.notify(message, appName: "cwtch", appIcon: iconPath.toString(), replacesId: this.previous_id).then((linux_notifications.Notification value) async {
previous_id = value.id;
if ((await value.closeReason) == linux_notifications.NotificationClosedReason.dismissed) {
this.notificationSelectConvo(profile, conversationId);
}
});
}
}
class NotificationPayload {
late String profileOnion;
late int convoId;
NotificationPayload(String po, int cid) {
profileOnion = po;
convoId = cid;
}
NotificationPayload.fromJson(Map<String, dynamic> json)
: profileOnion = json['profileOnion'],
convoId = json['convoId'];
Map<String, dynamic> toJson() => {
'profileOnion': profileOnion,
'convoId': convoId,
};
}
// FlutterLocalNotificationsPlugin based NotificationManager that handles MacOS
// Todo: work with author to allow settings of asset_path so we can use this for Linux and deprecate the LinuxNotificationManager
// Todo: it can also handle Android, do we want to migrate away from our manual solution?
class NixNotificationManager implements NotificationsManager {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
late Future<void> Function(String, int) notificationSelectConvo;
NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) { NixNotificationManager(Future<void> Function(String, int) notificationSelectConvo) {
this.notificationSelectConvo = notificationSelectConvo; this.notificationSelectConvo = notificationSelectConvo;
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: AssetsLinuxIcon('assets/knott.png'), defaultSuppressSound: true);
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
scheduleMicrotask(() async { scheduleMicrotask(() async {
if (Platform.isLinux) {
linuxAssetsPath = await detectLinuxAssetsPath();
} else {
linuxAssetsPath = "";
}
final MacOSInitializationSettings initializationSettingsMacOS = MacOSInitializationSettings(defaultPresentSound: false);
var linuxIcon = FilePathLinuxIcon( path.join(linuxAssetsPath, 'assets/knott.png'));
final LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification', defaultIcon: linuxIcon, defaultSuppressSound: true);
final InitializationSettings initializationSettings = InitializationSettings(android: null, iOS: null, macOS: initializationSettingsMacOS, linux: initializationSettingsLinux);
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions( flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()?.requestPermissions(
alert: true, alert: true,
badge: false, badge: false,
@ -150,7 +136,8 @@ class NixNotificationManager implements NotificationsManager {
Future<void> notify(String message, String profile, int conversationId) async { Future<void> notify(String message, String profile, int conversationId) async {
if (!globalAppState.focus) { if (!globalAppState.focus) {
// Warning: Only use title field on Linux, body field will render links as clickable // Warning: Only use title field on Linux, body field will render links as clickable
await flutterLocalNotificationsPlugin.show(0, message, '', NotificationDetails(linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived())), await flutterLocalNotificationsPlugin.show(0, message, '',
NotificationDetails(linux: LinuxNotificationDetails(suppressSound: true, category: LinuxNotificationCategory.imReceived(), icon: FilePathLinuxIcon(path.join(linuxAssetsPath, 'assets/knott.png')))),
payload: jsonEncode(NotificationPayload(profile, conversationId))); payload: jsonEncode(NotificationPayload(profile, conversationId)));
} }
} }
@ -168,7 +155,7 @@ class NixNotificationManager implements NotificationsManager {
NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) { NotificationsManager newDesktopNotificationsManager(Future<void> Function(String profileOnion, int convoId) notificationSelectConvo) {
if (Platform.isLinux && !Platform.isAndroid) { if (Platform.isLinux && !Platform.isAndroid) {
try { try {
return LinuxNotificationsManager(notificationSelectConvo); return NixNotificationManager(notificationSelectConvo);
} catch (e) { } catch (e) {
EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications."); EnvironmentConfig.debugLog("Failed to create LinuxNotificationManager. Switching off notifications.");
} }

View File

@ -87,7 +87,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
child: Container( child: Container(
margin: EdgeInsets.all(30), margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20), padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty, visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
@ -127,9 +127,6 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
badgeEdit: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)))) badgeEdit: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment))))
])), ])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),
SizedBox( SizedBox(
height: 20, height: 20,
@ -276,71 +273,64 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox( SizedBox(
height: 20, height: 20,
), ),
ElevatedButton( Row(
onPressed: _createPressed, mainAxisAlignment: MainAxisAlignment.center,
style: ElevatedButton.styleFrom( children: [
minimumSize: Size(400, 50), Expanded(
maximumSize: Size(800, 50), child: ElevatedButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))), onPressed: _createPressed,
), child: Text(
child: Text( Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn, textAlign: TextAlign.center,
textAlign: TextAlign.center, ),
), ),
), ),
SizedBox( ],
height: 20,
), ),
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty, visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Tooltip( child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
message: AppLocalizations.of(context)!.exportProfileTooltip, SizedBox(
child: ElevatedButton.icon( height: 20,
style: ElevatedButton.styleFrom( ),
minimumSize: Size(400, 50), Tooltip(
maximumSize: Size(800, 50), message: AppLocalizations.of(context)!.exportProfileTooltip,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))), child: ElevatedButton.icon(
), onPressed: () {
onPressed: () { if (Platform.isAndroid) {
if (Platform.isAndroid) { Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz");
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, ctrlrOnion.value.text + ".tar.gz"); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz"));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + ctrlrOnion.value.text + ".tar.gz")); ScaffoldMessenger.of(context).showSnackBar(snackBar);
ScaffoldMessenger.of(context).showSnackBar(snackBar); } else {
} else { showCreateFilePicker(context).then((name) {
showCreateFilePicker(context).then((name) { if (name != null) {
if (name != null) { Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name);
Provider.of<FlwtchState>(context, listen: false).cwtch.ExportProfile(ctrlrOnion.value.text, name); final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.fileSavedTo + " " + name)); ScaffoldMessenger.of(context).showSnackBar(snackBar);
ScaffoldMessenger.of(context).showSnackBar(snackBar); }
} });
}); }
} },
}, icon: Icon(Icons.import_export),
icon: Icon(Icons.import_export), label: Text(AppLocalizations.of(context)!.exportProfile),
label: Text(AppLocalizations.of(context)!.exportProfile), ))
))), ])),
SizedBox(
height: 20,
),
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty, visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Tooltip( child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete, SizedBox(
child: ElevatedButton.icon( height: 20,
style: ElevatedButton.styleFrom( ),
minimumSize: Size(400, 50), Tooltip(
maximumSize: Size(800, 50), message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete,
shape: RoundedRectangleBorder( child: ElevatedButton.icon(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0), onPressed: () {
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))), showAlertDialog(context);
primary: Provider.of<Settings>(context).theme.backgroundMainColor, },
), icon: Icon(Icons.delete_forever),
onPressed: () { label: Text(AppLocalizations.of(context)!.deleteBtn),
showAlertDialog(context); ))
}, ]))
icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn),
)))
])))))); ]))))));
}); });
}); });
@ -348,8 +338,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
void _copyOnion() { void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification)); // TODO Toast
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
void _createPressed() async { void _createPressed() async {

View File

@ -12,7 +12,6 @@ import 'package:cwtch/widgets/profileimage.dart';
import 'package:cwtch/widgets/textfield.dart'; import 'package:cwtch/widgets/textfield.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../main.dart'; import '../main.dart';
import '../settings.dart'; import '../settings.dart';
import 'addcontactview.dart'; import 'addcontactview.dart';
@ -30,22 +29,20 @@ class ContactsView extends StatefulWidget {
// selectConversation can be called from anywhere to set the active conversation // selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, int handle) { void selectConversation(BuildContext context, int handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
var unread = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages; var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation; var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
if (previouslySelected != null) { if (previouslySelected != null) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected(); Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
} }
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected(); Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
// triggers update in Double/TripleColumnView // triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread; Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
Provider.of<AppState>(context, listen: false).selectedConversation = handle; Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
Provider.of<AppState>(context, listen: false).hoveredIndex = -1; Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
// if in singlepane mode, push to the stack // if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context); var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
// Set last message seen time in backend // Set last message seen time in backend
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -88,35 +85,29 @@ class _ContactsViewState extends State<ContactsView> {
endDrawerEnableOpenDragGesture: false, endDrawerEnableOpenDragGesture: false,
drawerEnableOpenDragGesture: false, drawerEnableOpenDragGesture: false,
appBar: AppBar( appBar: AppBar(
leading: Stack(children: [ leading: Row(children: [
Align( IconButton(
alignment: Alignment.center, icon: Icon(Icons.arrow_back),
child: IconButton( tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: Icon(Icons.arrow_back), onPressed: () {
tooltip: MaterialLocalizations.of(context).backButtonTooltip, Provider.of<ProfileInfoState>(context, listen: false).recountUnread();
onPressed: () { Provider.of<AppState>(context, listen: false).selectedProfile = "";
Provider.of<ProfileInfoState>(context, listen: false).recountUnread(); Navigator.of(context).pop();
Provider.of<AppState>(context, listen: false).selectedProfile = ""; },
Navigator.of(context).pop(); ),
}, StreamBuilder<bool>(
)), stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
Positioned( builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
bottom: 5.0, int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
right: 5.0,
child: StreamBuilder<bool>(
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
return Visibility( return Visibility(
visible: unreadCount > 0, visible: unreadCount > 0,
child: CircleAvatar( child: CircleAvatar(
radius: 10.0, radius: 10.0,
backgroundColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor, backgroundColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)), child: Text(unreadCount > 99 ? "99+" : unreadCount.toString(), style: TextStyle(color: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, fontSize: 8.0)),
)); ));
}), }),
)
]), ]),
title: RepaintBoundary( title: RepaintBoundary(
child: Row(children: [ child: Row(children: [
@ -164,8 +155,6 @@ class _ContactsViewState extends State<ContactsView> {
splashRadius: Material.defaultSplashRadius / 2, splashRadius: Material.defaultSplashRadius / 2,
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
})); }));
// Manage known Servers // Manage known Servers
@ -215,28 +204,11 @@ class _ContactsViewState extends State<ContactsView> {
); );
}); });
var initialScroll = final divided = ListTile.divideTiles(
Provider.of<ProfileInfoState>(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == Provider.of<AppState>(context).selectedConversation); context: context,
if (initialScroll < 0) { tiles: tiles,
initialScroll = 0; ).toList();
} return RepaintBoundary(child: ListView(children: divided));
var contactList = ScrollablePositionedList.separated(
itemScrollController: Provider.of<ProfileInfoState>(context).contactListScrollController,
itemCount: Provider.of<ContactListState>(context).num,
initialScrollIndex: initialScroll,
shrinkWrap: true,
physics: BouncingScrollPhysics(),
semanticChildCount: Provider.of<ContactListState>(context).num,
itemBuilder: (context, index) {
return tiles.elementAt(index);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 1);
},
);
return RepaintBoundary(child: contactList);
} }
void _pushAddContact(bool newGroup) { void _pushAddContact(bool newGroup) {
@ -282,86 +254,61 @@ class _ContactsViewState extends State<ContactsView> {
height: 200, // bespoke value courtesy of the [TextField] docs height: 200, // bespoke value courtesy of the [TextField] docs
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(2.0), padding: EdgeInsets.all(10.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: AppLocalizations.of(context)!.tooltipAddContact,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addContact, semanticsLabel: AppLocalizations.of(context)!.addContact),
onPressed: () {
_pushAddContact(false);
},
))),
Spacer()
]),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Expanded( Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip( child: Tooltip(
message: AppLocalizations.of(context)!.tooltipAddContact, message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( child: Text(AppLocalizations.of(context)!.addServerTitle, semanticsLabel: AppLocalizations.of(context)!.addServerTitle),
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addContact,
semanticsLabel: AppLocalizations.of(context)!.addContact,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: () {
_pushAddContact(false);
},
))),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.addServerTooltip : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.addServerTitle,
semanticsLabel: AppLocalizations.of(context)!.addServerTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled
? () {
_pushAddContact(false);
}
: null,
)),
),
SizedBox(
height: 20,
),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.fromWidth(double.infinity),
maximumSize: Size.fromWidth(400),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
),
child: Text(
AppLocalizations.of(context)!.createGroupTitle,
semanticsLabel: AppLocalizations.of(context)!.createGroupTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
onPressed: groupsEnabled onPressed: groupsEnabled
? () { ? () {
_pushAddContact(true); _pushAddContact(false);
} }
: null, : null,
))), )),
),
Spacer()
]),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: Tooltip(
message: groupsEnabled ? AppLocalizations.of(context)!.createGroupTitle : AppLocalizations.of(context)!.thisFeatureRequiresGroupExpermientsToBeEnabled,
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.createGroupTitle, semanticsLabel: AppLocalizations.of(context)!.createGroupTitle),
onPressed: groupsEnabled
? () {
_pushAddContact(true);
}
: null,
))),
Spacer()
]),
], ],
))), ))),
))); )));

View File

@ -161,7 +161,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")), child: Text(getThemeName(context, themeId)), //"ddi_$themeId", key: Key("ddi_$themeId")),
); );
}).toList())), }).toList())),
leading: Icon(Icons.palette, color: settings.current().mainTextColor), leading: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor),
), ),
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)), title: Text(AppLocalizations.of(context)!.settingUIColumnPortrait, style: TextStyle(color: settings.current().mainTextColor)),
@ -188,7 +188,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
softWrap: true, softWrap: true,
style: TextStyle(color: settings.current().mainTextColor), style: TextStyle(color: settings.current().mainTextColor),
), ),
leading: Icon(Icons.stay_primary_landscape, color: settings.current().mainTextColor), leading: Icon(Icons.table_chart, color: settings.current().mainTextColor),
trailing: Container( trailing: Container(
width: MediaQuery.of(context).size.width / 4, width: MediaQuery.of(context).size.width / 4,
child: Container( child: Container(
@ -406,7 +406,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor, activeTrackColor: settings.theme.defaultButtonActiveColor,
inactiveTrackColor: settings.theme.defaultButtonDisabledColor, inactiveTrackColor: settings.theme.defaultButtonDisabledColor,
secondary: Icon(Icons.photo, color: settings.current().mainTextColor), secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor),
), ),
Visibility( Visibility(
visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid, visible: settings.isExperimentEnabled(ImagePreviewsExperiment) && !Platform.isAndroid,

View File

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cwtch/cwtch/cwtch.dart'; import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
@ -11,7 +10,6 @@ import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messagecache.dart'; import 'package:cwtch/models/messagecache.dart';
import 'package:cwtch/models/messages/quotedmessage.dart'; import 'package:cwtch/models/messages/quotedmessage.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
@ -44,9 +42,8 @@ class _MessageViewState extends State<MessageView> {
final focusNode = FocusNode(); final focusNode = FocusNode();
int selectedContact = -1; int selectedContact = -1;
ItemPositionsListener scrollListener = ItemPositionsListener.create(); ItemPositionsListener scrollListener = ItemPositionsListener.create();
ItemScrollController scrollController = ItemScrollController();
File? imagePreview; File? imagePreview;
bool showDown = false;
bool showPreview = false;
@override @override
void initState() { void initState() {
@ -57,12 +54,6 @@ class _MessageViewState extends State<MessageView> {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0; Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false; Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
} }
if (scrollListener.itemPositions.value.length != 0 && !scrollListener.itemPositions.value.any((element) => element.index == 0)) {
showDown = true;
} else {
showDown = false;
}
}); });
super.initState(); super.initState();
} }
@ -90,11 +81,10 @@ class _MessageViewState extends State<MessageView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// After leaving a conversation the selected conversation is set to null... // After leaving a conversation the selected conversation is set to null...
if (Provider.of<ContactInfoState>(context, listen: false).profileOnion == "") { if (Provider.of<ContactInfoState>(context).profileOnion == "") {
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst))); return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
} }
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment); var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
var appBarButtons = <Widget>[]; var appBarButtons = <Widget>[];
if (Provider.of<ContactInfoState>(context).isOnline()) { if (Provider.of<ContactInfoState>(context).isOnline()) {
@ -138,26 +128,18 @@ class _MessageViewState extends State<MessageView> {
onWillPop: _onWillPop, onWillPop: _onWillPop,
child: Scaffold( child: Scaffold(
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor, backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
floatingActionButton: showDown floatingActionButton: appState.unreadMessagesBelow
? FloatingActionButton( ? FloatingActionButton(
child: Icon(Icons.arrow_downward, color: Provider.of<Settings>(context).current().defaultButtonTextColor), child: Icon(Icons.arrow_downward, color: Provider.of<Settings>(context).current().defaultButtonTextColor),
onPressed: () { onPressed: () {
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0; Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false; Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
Provider.of<ContactInfoState>(context, listen: false).messageScrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600)); scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
}) })
: null, : null,
appBar: AppBar( appBar: AppBar(
// setting leading(Width) to null makes it do the default behaviour; container() hides it // setting leading to null makes it do the default behaviour; container() hides it
leadingWidth: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? 0 : null, leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1
? Container(
padding: EdgeInsets.zero,
margin: EdgeInsets.zero,
width: 0,
height: 0,
)
: null,
title: Row(children: [ title: Row(children: [
ProfileImage( ProfileImage(
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
@ -179,12 +161,8 @@ class _MessageViewState extends State<MessageView> {
]), ]),
actions: appBarButtons, actions: appBarButtons,
), ),
body: Padding( body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 182.0), bottomSheet: _buildComposeBox(),
child: MessageList(
scrollListener,
)),
bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(),
)); ));
} }
@ -233,7 +211,7 @@ class _MessageViewState extends State<MessageView> {
ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine, selection: TextSelection.fromPosition(TextPosition(offset: messageWithoutNewLine.length))); ctrlrCompose.value = TextEditingValue(text: messageWithoutNewLine, selection: TextSelection.fromPosition(TextPosition(offset: messageWithoutNewLine.length)));
// Do this after we trim to preserve enter-behaviour... // Do this after we trim to preserve enter-behaviour...
bool isOffline = Provider.of<ContactInfoState>(context, listen: false).isOnline() == false; bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
if (isOffline) { if (isOffline) {
return; return;
} }
@ -317,224 +295,64 @@ class _MessageViewState extends State<MessageView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String()); Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String());
} }
Widget _buildPreviewBox() { Widget _buildComposeBox() {
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment); bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
var wdgMessage = Padding( var charLength = ctrlrCompose.value.text.characters.length;
padding: EdgeInsets.all(8), var expectedLength = ctrlrCompose.value.text.length;
child: SelectableLinkify( var numberOfBytesMoreThanChar = (expectedLength - charLength);
text: ctrlrCompose.text + '\n',
options: LinkifyOptions(messageFormatting: true, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks ? null : null,
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 16,
),
linkStyle: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
fontSize: 16,
),
codeStyle: TextStyle(
// note: these colors are flipped
fontSize: 16,
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
backgroundColor: Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
));
var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var preview = showMessageFormattingPreview
? IconButton(
tooltip: AppLocalizations.of(context)!.tooltipBackToMessageEditing,
icon: Icon(Icons.text_fields),
onPressed: () {
setState(() {
showPreview = false;
});
})
: Container();
var composeBox = Container( var composeBox = Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor, color: Provider.of<Settings>(context).theme.backgroundMainColor,
padding: EdgeInsets.all(2), padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2), margin: EdgeInsets.all(2),
height: 100,
// 164 minimum height + 16px for every line of text so the entire message is displayed when previewed. child: Row(
height: 164 + ((ctrlrCompose.text.split("\n").length - 1) * 16), children: <Widget>[
child: Column( Expanded(
children: [ child: Container(
Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [preview]), decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
Container( child: RawKeyboardListener(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))), focusNode: FocusNode(),
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [wdgMessage])), onKey: handleKeyPress,
child: Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
enableIMEPersonalizedLearning: false,
minLines: 1,
maxLength: (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
maxLines: null,
onFieldSubmitted: _sendMessage,
enabled: true, // always allow editing...
onChanged: (String x) {
setState(() {
// we need to force a rerender here to update the max length count
});
},
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.sendHintTextColor),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
suffixIcon: ElevatedButton(
key: Key("btnSend"),
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))),
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
onPressed: isOffline ? null : _sendMessage,
))),
)))),
], ],
), ),
); );
return Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]));
}
Widget _buildComposeBox() {
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
var charLength = ctrlrCompose.value.text.characters.length;
var expectedLength = ctrlrCompose.value.text.length;
var numberOfBytesMoreThanChar = (expectedLength - charLength);
var bold = IconButton(
icon: Icon(Icons.format_bold),
tooltip: AppLocalizations.of(context)!.tooltipBoldText,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "**" + selected + "**");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
});
});
var italic = IconButton(
icon: Icon(Icons.format_italic),
tooltip: AppLocalizations.of(context)!.tooltipItalicize,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "*" + selected + "*");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var code = IconButton(
icon: Icon(Icons.code),
tooltip: AppLocalizations.of(context)!.tooltipCode,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "`" + selected + "`");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var superscript = IconButton(
icon: Icon(Icons.superscript),
tooltip: AppLocalizations.of(context)!.tooltipSuperscript,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "^" + selected + "^");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var subscript = IconButton(
icon: Icon(Icons.subscript),
tooltip: AppLocalizations.of(context)!.tooltipSubscript,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "_" + selected + "_");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
});
});
var strikethrough = IconButton(
icon: Icon(Icons.format_strikethrough),
tooltip: AppLocalizations.of(context)!.tooltipStrikethrough,
onPressed: () {
setState(() {
var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
var selection = ctrlrCompose.selection;
var start = ctrlrCompose.selection.start;
var end = ctrlrCompose.selection.end;
ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "~~" + selected + "~~");
ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
});
});
var preview = IconButton(
icon: Icon(Icons.text_format),
tooltip: AppLocalizations.of(context)!.tooltipPreviewFormatting,
onPressed: () {
setState(() {
showPreview = true;
});
});
var vline = Padding(
padding: EdgeInsets.symmetric(vertical: 1, horizontal: 2),
child: Container(height: 16, width: 1, decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)));
var formattingToolbar = Container(
decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor),
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [bold, italic, code, superscript, subscript, strikethrough, vline, preview]));
var textField = Container(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor))),
child: RawKeyboardListener(
focusNode: FocusNode(),
onKey: handleKeyPress,
child: Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
key: Key('txtCompose'),
controller: ctrlrCompose,
focusNode: focusNode,
autofocus: !Platform.isAndroid,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
enableIMEPersonalizedLearning: false,
minLines: 1,
maxLength: (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
maxLines: 3,
onFieldSubmitted: _sendMessage,
enabled: true, // always allow editing...
onChanged: (String x) {
setState(() {
// we need to force a rerender here to update the max length count
});
},
decoration: InputDecoration(
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.sendHintTextColor),
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
enabled: true,
suffixIcon: ElevatedButton(
key: Key("btnSend"),
style: ElevatedButton.styleFrom(padding: EdgeInsets.all(0.0), shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0))),
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
onPressed: isOffline ? null : _sendMessage,
))),
)));
var textEditChildren;
if (showToolbar) {
textEditChildren = [formattingToolbar, textField];
} else {
textEditChildren = [textField];
}
var composeBox =
Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), height: 164, child: Column(children: textEditChildren));
var children; var children;
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) { if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
@ -583,7 +401,7 @@ class _MessageViewState extends State<MessageView> {
children = [composeBox]; children = [composeBox];
} }
return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: children)); return Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, children: children));
} }
// Send the message if enter is pressed without the shift key... // Send the message if enter is pressed without the shift key...

View File

@ -140,7 +140,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
CwtchButtonTextField( CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion), controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion, onPressed: _copyOnion,
icon: Icon(CwtchIcons.address_copy_2), icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context)!.copyBtn, tooltip: AppLocalizations.of(context)!.copyBtn,
) )
]), ]),

View File

@ -194,66 +194,49 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
padding: EdgeInsets.all(10.0), padding: EdgeInsets.all(10.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
Spacer(),
Expanded(
child: ElevatedButton(
child: Text(AppLocalizations.of(context)!.addProfileTitle, semanticsLabel: AppLocalizations.of(context)!.addProfileTitle),
onPressed: () {
_pushAddProfile(context);
},
)),
Spacer()
]),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Expanded( Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
child: ElevatedButton( Spacer(),
style: ElevatedButton.styleFrom( Expanded(
minimumSize: Size(double.infinity, 20), child: Tooltip(
maximumSize: Size(400, 20), message: AppLocalizations.of(context)!.importProfileTooltip,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))), child: ElevatedButton(
), child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
child: Text( onPressed: () {
AppLocalizations.of(context)!.addProfileTitle, // 10GB profiles should be enough for anyone?
semanticsLabel: AppLocalizations.of(context)!.addProfileTitle, showFilePicker(context, MaxGeneralFileSharingSize, (file) {
style: TextStyle(fontWeight: FontWeight.bold), showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
), Navigator.popUntil(context, (route) => route.isFirst);
onPressed: () { Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
_pushAddProfile(context); if (value == "") {
}, final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
)), ScaffoldMessenger.of(context).showSnackBar(snackBar);
SizedBox( } else {
height: 20, final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
), ScaffoldMessenger.of(context).showSnackBar(snackBar);
Expanded( }
child: Tooltip( });
message: AppLocalizations.of(context)!.importProfileTooltip,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 20),
maximumSize: Size(400, 20),
shape: RoundedRectangleBorder(
side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180))),
primary: Provider.of<Settings>(context).theme.backgroundMainColor,
),
child:
Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile, style: TextStyle(fontWeight: FontWeight.bold)),
onPressed: () {
// 10GB profiles should be enough for anyone?
showFilePicker(context, MaxGeneralFileSharingSize, (file) {
showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
Navigator.popUntil(context, (route) => route.isFirst);
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
if (value == "") {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} else {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}); });
}); }, () {}, () {});
}, () {}, () {}); },
}, ))),
))), Spacer()
SizedBox( ]),
height: 20,
),
], ],
))), ))),
))); )));

View File

@ -3,8 +3,11 @@ import 'dart:io';
import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/models/profileservers.dart';
import 'package:cwtch/views/contactsview.dart'; import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:cwtch/views/messageview.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -77,52 +80,44 @@ class _ContactRowState extends State<ContactRow> {
Visibility( Visibility(
visible: !Provider.of<Settings>(context).streamerMode, visible: !Provider.of<Settings>(context).streamerMode,
child: Text(contact.onion, child: Text(contact.onion,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor : Provider.of<Settings>(context).theme.mainTextColor)), style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor : Provider.of<Settings>(context).theme.mainTextColor)),
), )
Container(
padding: EdgeInsets.all(0),
child: contact.isInvitation == true
? Wrap(alignment: WrapAlignment.start, direction: Axis.vertical, children: <Widget>[
Padding(
padding: EdgeInsets.all(2),
child: TextButton.icon(
label: Text(
AppLocalizations.of(context)!.tooltipAcceptContactRequest,
),
icon: Icon(
Icons.favorite,
size: 16,
color: Provider.of<Settings>(context).theme.mainTextColor,
),
onPressed: _btnApprove,
)),
Padding(
padding: EdgeInsets.all(2),
child: TextButton.icon(
label: Text(
AppLocalizations.of(context)!.tooltipRejectContactRequest,
style: TextStyle(decoration: TextDecoration.underline),
),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.backgroundPaneColor),
foregroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.mainTextColor)),
icon: Icon(Icons.delete, size: 16, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: _btnReject,
))
])
: (contact.isBlocked != null && contact.isBlocked
? IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: () {},
)
: Text(dateToNiceString(contact.lastMessageTime))),
),
], ],
))), ))),
Padding(
padding: const EdgeInsets.all(5.0),
child: contact.isInvitation == true
? Wrap(direction: Axis.vertical, children: <Widget>[
IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(
Icons.favorite,
color: Provider.of<Settings>(context).theme.mainTextColor,
),
tooltip: AppLocalizations.of(context)!.tooltipAcceptContactRequest,
onPressed: _btnApprove,
),
IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.delete, color: Provider.of<Settings>(context).theme.mainTextColor),
tooltip: AppLocalizations.of(context)!.tooltipRejectContactRequest,
onPressed: _btnReject,
)
])
: (contact.isBlocked != null && contact.isBlocked
? IconButton(
padding: EdgeInsets.zero,
splashRadius: Material.defaultSplashRadius / 2,
iconSize: 16,
icon: Icon(Icons.block, color: Provider.of<Settings>(context).theme.mainTextColor),
onPressed: () {},
)
: Text(dateToNiceString(contact.lastMessageTime))),
),
]), ]),
onTap: () { onTap: () {
selectConversation(context, contact.identifier); selectConversation(context, contact.identifier);
@ -153,7 +148,7 @@ class _ContactRowState extends State<ContactRow> {
return AppLocalizations.of(context)!.conversationNotificationPolicyNever; return AppLocalizations.of(context)!.conversationNotificationPolicyNever;
} }
// If the last message was over a day ago, just state the date // If the last message was over a day ago, just state the date
if (DateTime.now().difference(date).inDays > 0) { if (DateTime.now().difference(date).inDays > 1) {
return DateFormat.yMd(Platform.localeName).format(date.toLocal()); return DateFormat.yMd(Platform.localeName).format(date.toLocal());
} }
// Otherwise just state the time. // Otherwise just state the time.

View File

@ -25,9 +25,8 @@ class FileBubble extends StatefulWidget {
final int fileSize; final int fileSize;
final bool interactive; final bool interactive;
final bool isAuto; final bool isAuto;
final bool isPreview;
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true, this.isPreview = false}); FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true});
@override @override
FileBubbleState createState() => FileBubbleState(); FileBubbleState createState() => FileBubbleState();
@ -45,22 +44,6 @@ class FileBubbleState extends State<FileBubble> {
super.initState(); super.initState();
} }
Widget getPreview(context) {
return Image.file(
myFile!,
cacheWidth: (MediaQuery.of(context).size.width * 0.6).floor(),
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
@ -126,12 +109,6 @@ class FileBubbleState extends State<FileBubble> {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle; senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
} }
} }
// we don't preview a non downloaded file...
if (widget.isPreview && myFile != null) {
return getPreview(context);
}
return LayoutBuilder(builder: (bcontext, constraints) { return LayoutBuilder(builder: (bcontext, constraints) {
var wdgSender = Visibility( var wdgSender = Visibility(
visible: widget.interactive, visible: widget.interactive,
@ -156,7 +133,21 @@ class FileBubbleState extends State<FileBubble> {
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
child: Padding(padding: EdgeInsets.all(1.0), child: getPreview(context)), child: Padding(
padding: EdgeInsets.all(1.0),
child: Image.file(
myFile!,
cacheWidth: (MediaQuery.of(bcontext).size.width * 0.6).floor(),
// limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
filterQuality: FilterQuality.medium,
fit: BoxFit.scaleDown,
alignment: Alignment.center,
height: MediaQuery.of(bcontext).size.height * 0.30,
isAntiAlias: false,
errorBuilder: (context, error, stackTrace) {
return MalformedBubble();
},
)),
onTap: () { onTap: () {
pop(bcontext, myFile!, wdgMessage); pop(bcontext, myFile!, wdgMessage);
}, },

View File

@ -1,13 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'package:cwtch/controllers/open_link_modal.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
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/flutter_linkify.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.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_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
@ -52,7 +55,7 @@ class MessageBubbleState extends State<MessageBubble> {
linkifiers: [UrlLinkifier()], linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks onOpen: showClickableLinks
? (link) { ? (link) {
modalOpenLink(context, link); _modalOpenLink(context, link);
} }
: null, : null,
//key: Key(myKey), //key: Key(myKey),
@ -101,4 +104,59 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
}); });
} }
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
context: ctx,
builder: (BuildContext bcontext) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
child: Center(
child: Padding(
padding: EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(bcontext)!.clickableLinksWarning),
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinksCopy, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinksCopy),
onPressed: () {
Clipboard.setData(new ClipboardData(text: link.url));
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.copiedToClipboardNotification),
);
Navigator.pop(bcontext);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
},
),
),
Container(
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.clickableLinkOpen, semanticsLabel: AppLocalizations.of(bcontext)!.clickableLinkOpen),
onPressed: () async {
if (await canLaunch(link.url)) {
await launch(link.url);
Navigator.pop(bcontext);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(bcontext)!.clickableLinkError),
);
ScaffoldMessenger.of(bcontext).showSnackBar(snackBar);
}
},
),
),
]),
],
)),
));
});
}
} }

View File

@ -13,8 +13,9 @@ import '../main.dart';
import '../settings.dart'; import '../settings.dart';
class MessageList extends StatefulWidget { class MessageList extends StatefulWidget {
ItemScrollController scrollController;
ItemPositionsListener scrollListener; ItemPositionsListener scrollListener;
MessageList(this.scrollListener); MessageList(this.scrollController, this.scrollListener);
@override @override
_MessageListState createState() => _MessageListState(); _MessageListState createState() => _MessageListState();
@ -29,6 +30,7 @@ class _MessageListState extends State<MessageList> {
MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache; MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!); ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
} }
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex; var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup; bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated"; bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
@ -80,7 +82,7 @@ class _MessageListState extends State<MessageList> {
child: loadMessages child: loadMessages
? ScrollablePositionedList.builder( ? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener, itemPositionsListener: widget.scrollListener,
itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController, itemScrollController: widget.scrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0, initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages, itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction... reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...

View File

@ -12,7 +12,6 @@ import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../main.dart'; import '../main.dart';
import '../settings.dart'; import '../settings.dart';
@ -275,8 +274,6 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
// Can't happen // Can't happen
} else { } else {
selectConversation(context, id); selectConversation(context, id);
var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == id);
Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.jumpTo(index: contactIndex);
} }
} }

View File

@ -1,13 +1,8 @@
import 'package:cwtch/controllers/open_link_modal.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -48,29 +43,12 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor)); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor));
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment); var wdgMessage = SelectableText(
var formatMessages = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment); widget.body + '\u202F',
var wdgMessage = SelectableLinkify(
text: widget.body + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
options: LinkifyOptions(messageFormatting: formatMessages, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
linkifiers: [UrlLinkifier()],
onOpen: showClickableLinks
? (link) {
modalOpenLink(context, link);
}
: null,
//key: Key(myKey),
focusNode: _focus, focusNode: _focus,
style: TextStyle( style: TextStyle(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor,
), ),
linkStyle: TextStyle(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor : Provider.of<Settings>(context).theme.messageFromOtherTextColor),
codeStyle: TextStyle(
// note: these colors are flipped
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor,
backgroundColor: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor),
textAlign: TextAlign.left, textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine, textWidthBasis: TextWidthBasis.longestLine,
); );
@ -83,33 +61,14 @@ class QuotedMessageBubbleState extends State<QuotedMessageBubble> {
var qMessage = (snapshot.data! as Message); var qMessage = (snapshot.data! as Message);
// Swap the background color for quoted tweets.. // Swap the background color for quoted tweets..
var qTextColor = fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor; var qTextColor = fromMe ? Provider.of<Settings>(context).theme.messageFromOtherTextColor : Provider.of<Settings>(context).theme.messageFromMeTextColor;
return MouseRegion( return Container(
cursor: SystemMouseCursors.click, margin: EdgeInsets.all(5),
child: GestureDetector( padding: EdgeInsets.all(5),
onTap: () { color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
var index = Provider.of<ContactInfoState>(context, listen: false).messageCache.cacheByHash[qMessage.getMetadata().contenthash]; child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [
var totalMessages = Provider.of<ContactInfoState>(context, listen: false).totalMessages; Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32, color: qTextColor))),
// we have to reverse here because the list itself is reversed... Center(widthFactor: 1.0, child: DefaultTextStyle(child: qMessage.getPreviewWidget(context), style: TextStyle(color: qTextColor)))
Provider.of<ContactInfoState>(context).messageScrollController.scrollTo(index: totalMessages - index!, duration: Duration(milliseconds: 100)); ]));
},
child: Container(
margin: EdgeInsets.all(5),
padding: EdgeInsets.all(5),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: fromMe ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
),
height: 75,
child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [
Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(Icons.reply, size: 32, color: qTextColor)),
Flexible(
child: DefaultTextStyle(
textWidthBasis: TextWidthBasis.parent,
child: qMessage.getPreviewWidget(context),
style: TextStyle(color: qTextColor),
overflow: TextOverflow.fade,
))
]))));
} catch (e) { } catch (e) {
print(e); print(e);
return MalformedBubble(); return MalformedBubble();

View File

@ -61,8 +61,6 @@ class _ServerRowState extends State<ServerRow> {
icon: Icon(CwtchIcons.address_copy_2, color: Provider.of<Settings>(context).current().mainTextColor), icon: Icon(CwtchIcons.address_copy_2, color: Provider.of<Settings>(context).current().mainTextColor),
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: server.serverBundle)); Clipboard.setData(new ClipboardData(text: server.serverBundle));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedToClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, },
), ),

View File

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@ -3,10 +3,14 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever
url_launcher_linux url_launcher_linux
window_manager window_manager
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST}) foreach(plugin ${FLUTTER_PLUGIN_LIST})
@ -15,3 +19,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin) endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@ -5,12 +5,18 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import flutter_local_notifications
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import screen_retriever
import url_launcher_macos import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
} }

View File

@ -15,27 +15,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.0" version: "2.8.0"
ansicolor:
dependency: transitive
description:
name: ansicolor
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.6" version: "3.1.11"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -56,7 +49,7 @@ packages:
name: build name: build
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@ -70,7 +63,7 @@ packages:
name: build_daemon name: build_daemon
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.1.0"
build_resolvers: build_resolvers:
dependency: transitive dependency: transitive
description: description:
@ -84,7 +77,7 @@ packages:
name: build_runner name: build_runner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.7" version: "2.1.11"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
@ -105,7 +98,7 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.1.4" version: "8.3.3"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -127,6 +120,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
cli_dialog:
dependency: transitive
description:
name: cli_dialog
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
cli_util: cli_util:
dependency: transitive dependency: transitive
description: description:
@ -154,14 +154,14 @@ packages:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
convert: convert:
dependency: transitive dependency: transitive
description: description:
name: convert name: convert
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
crypto: crypto:
dependency: "direct main" dependency: "direct main"
description: description:
@ -175,7 +175,14 @@ packages:
name: cupertino_icons name: cupertino_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
dart_console:
dependency: transitive
description:
name: dart_console
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -189,7 +196,7 @@ packages:
name: dbus name: dbus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.1" version: "0.7.4"
desktop_notifications: desktop_notifications:
dependency: "direct main" dependency: "direct main"
description: description:
@ -203,14 +210,14 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
ffi: ffi:
dependency: "direct main" dependency: "direct main"
description: description:
name: ffi name: ffi
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.2" version: "1.2.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -224,7 +231,7 @@ packages:
name: file_picker name: file_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.3.3" version: "4.6.1"
file_picker_desktop: file_picker_desktop:
dependency: "direct main" dependency: "direct main"
description: description:
@ -238,7 +245,7 @@ packages:
name: fixnum name: fixnum
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -262,14 +269,14 @@ packages:
name: flutter_local_notifications name: flutter_local_notifications
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.3.2" version: "9.6.1"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_local_notifications_linux name: flutter_local_notifications_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.2" version: "0.5.0+1"
flutter_local_notifications_platform_interface: flutter_local_notifications_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -288,7 +295,7 @@ packages:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
flutter_test: flutter_test:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -305,12 +312,19 @@ packages:
name: frontend_server_client name: frontend_server_client
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
fuchsia_remote_debug_protocol: fuchsia_remote_debug_protocol:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
get_it:
dependency: transitive
description:
name: get_it
url: "https://pub.dartlang.org"
source: hosted
version: "7.2.0"
gherkin: gherkin:
dependency: transitive dependency: transitive
description: description:
@ -324,7 +338,7 @@ packages:
name: glob name: glob
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -345,21 +359,21 @@ packages:
name: http_multi_server name: http_multi_server
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.2.1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0" version: "4.0.1"
image: image:
dependency: transitive dependency: transitive
description: description:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.1" version: "3.2.0"
integration_test: integration_test:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -385,14 +399,14 @@ packages:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.4"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "4.5.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -407,6 +421,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -420,14 +441,14 @@ packages:
name: mime name: mime
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
msix: msix:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: msix name: msix
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.3" version: "3.6.2"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -441,21 +462,21 @@ packages:
name: package_config name: package_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.4.2"
package_info_plus_linux: package_info_plus_linux:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_linux name: package_info_plus_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.5"
package_info_plus_macos: package_info_plus_macos:
dependency: transitive dependency: transitive
description: description:
@ -476,84 +497,84 @@ packages:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_windows name: package_info_plus_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.8" version: "2.0.11"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.15"
path_provider_ios: path_provider_ios:
dependency: transitive dependency: transitive
description: description:
name: path_provider_ios name: path_provider_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.7" version: "2.0.10"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.7"
path_provider_macos: path_provider_macos:
dependency: transitive dependency: transitive
description: description:
name: path_provider_macos name: path_provider_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.6"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.0.7"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.4.0" version: "5.0.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.1.0"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -567,7 +588,7 @@ packages:
name: pool name: pool
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.5.1"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -581,14 +602,14 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.0" version: "6.0.3"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
name: pub_semver name: pub_semver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
pubspec_parse: pubspec_parse:
dependency: transitive dependency: transitive
description: description:
@ -596,27 +617,34 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
scrollable_positioned_list: scrollable_positioned_list:
dependency: "direct main" dependency: "direct main"
description: description:
name: scrollable_positioned_list name: scrollable_positioned_list
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.3" version: "0.3.2"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.3.1"
shelf_web_socket: shelf_web_socket:
dependency: transitive dependency: transitive
description: description:
name: shelf_web_socket name: shelf_web_socket
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -628,14 +656,14 @@ packages:
name: source_gen name: source_gen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.1" version: "1.8.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -684,7 +712,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.9"
timezone: timezone:
dependency: transitive dependency: transitive
description: description:
@ -712,70 +740,70 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.18" version: "6.1.3"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.14" version: "6.0.17"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.14" version: "6.0.17"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "3.0.1"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "3.0.1"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.1.0"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.12"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "3.0.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.3.0" version: "8.2.2"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -789,7 +817,7 @@ packages:
name: web_socket_channel name: web_socket_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
webdriver: webdriver:
dependency: transitive dependency: transitive
description: description:
@ -803,7 +831,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.8" version: "2.6.1"
win_toast: win_toast:
dependency: "direct main" dependency: "direct main"
description: description:
@ -817,7 +845,7 @@ packages:
name: window_manager name: window_manager
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.4" version: "0.2.5"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -831,14 +859,14 @@ packages:
name: xml name: xml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.3.1" version: "6.1.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
name: yaml name: yaml
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
sdks: sdks:
dart: ">=2.15.0 <3.0.0" dart: ">=2.17.0 <3.0.0"
flutter: ">=2.5.0" flutter: ">=3.0.0"

View File

@ -23,7 +23,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
provider: 5.0.0 provider: ^6.0.3
package_info_plus: ^1.0.0 package_info_plus: ^1.0.0
#intl_translation: any #intl_translation: any
flutter_localizations: flutter_localizations:
@ -32,23 +32,23 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
ffi: ^1.0.0 ffi: ^1.2.1
path_provider: ^2.0.0 path_provider: ^2.0.0
crypto: 3.0.1 crypto: 3.0.1
glob: any glob: any
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.3.2
file_picker: ^4.3.2 file_picker: ^4.3.2
file_picker_desktop: ^1.1.0 file_picker_desktop: ^1.1.1
url_launcher: ^6.0.18 url_launcher: ^6.0.18
window_manager: ^0.1.4 window_manager: ^0.2.5
# notification plugins # notification plugins
win_toast: ^0.0.2 win_toast: ^0.0.2
flutter_local_notifications: 9.3.2 flutter_local_notifications: ^9.6.1
desktop_notifications: ^0.6.3 desktop_notifications: ^0.6.3
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^3.6.2
flutter_gherkin: ^3.0.0-rc.9 flutter_gherkin: ^3.0.0-rc.9
build_runner: any build_runner: any
# integration_test: any # integration_test: any

View File

@ -6,11 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <win_toast/win_toast_plugin.h> #include <win_toast/win_toast_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WinToastPluginRegisterWithRegistrar( WinToastPluginRegisterWithRegistrar(

View File

@ -3,11 +3,15 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
screen_retriever
url_launcher_windows url_launcher_windows
win_toast win_toast
window_manager window_manager
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST}) foreach(plugin ${FLUTTER_PLUGIN_LIST})
@ -16,3 +20,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin) endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)