Compare commits

..

1 Commits

Author SHA1 Message Date
Sarah Jamie Lewis 1214b0ffbd Notes from Android Lifecycle Deep Dive
continuous-integration/drone/pr Build is passing Details
2021-05-20 17:36:43 -07:00
112 changed files with 2637 additions and 4786 deletions

View File

@ -1,3 +1,170 @@
---
kind: pipeline
type: docker
name: linux-android-test
clone:
disable: true
steps:
- name: clone
image: cirrusci/flutter:dev
environment:
buildbot_key_b64:
from_secret: buildbot_key_b64
commands:
- mkdir ~/.ssh
- echo $buildbot_key_b64 > ~/.ssh/id_rsa.b64
- base64 -d ~/.ssh/id_rsa.b64 > ~/.ssh/id_rsa
- chmod 400 ~/.ssh/id_rsa
# force by pass of ssh host key check, less secure
- ssh-keyscan -H git.openprivacy.ca >> ~/.ssh/known_hosts
- git clone gogs@git.openprivacy.ca:flutter/flutter_app.git .
- git checkout $DRONE_COMMIT
- name: fetch
image: cirrusci/flutter:dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- echo `git describe --tags` > VERSION
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
- flutter pub get
- mkdir deploy
- ./fetch-libcwtch-go.sh
#- name: quality
# image: golang
# volumes:
# - name: deps
# path: /go
# commands:
# - go list ./... | xargs go vet
# - go list ./... | xargs golint
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
- name: build-linux
image: openpriv/flutter-desktop:linux-dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
- flutter build linux
- mkdir deploy/linux
- cp -r build/linux/x64/release/bundle/* deploy/linux
- cp linux/cwtch.desktop deploy/linux
- cp linux/cwtch.png deploy/linux
- cp linux/libCwtch.so deploy/linux/lib/
- cp /sdks/flutter/bin/cache/artifacts/engine/linux-x64/icudtl.dat deploy/linux
- name: build-android
image: cirrusci/flutter:dev
when:
event: push
environment:
upload_jks_file_b64:
from_secret: upload_jks_file_b64
upload_jks_pass:
from_secret: upload_jks_pass
volumes:
- name: deps
path: /root/.pub-cache
commands:
- echo $upload_jks_file_b64 > upload-keystore.jks.b64
- base64 -i --decode upload-keystore.jks.b64 > android/app/upload-keystore.jks
- sed -i "s/%jks-password%/$upload_jks_pass/g" android/key.properties
- flutter build appbundle
# cant do debug for final release, this is just a stop gap
- flutter build apk
# or build apk --split-per-abi ?
- mkdir deploy/android
- cp build/app/outputs/bundle/release/app-release.aab deploy/android
- cp build/app/outputs/apk/release/app-release.apk deploy/android
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
- name: widget-tests
image: cirrusci/flutter:dev
volumes:
- name: deps
path: /root/.pub-cache
commands:
# - flutter config --enable-linux-desktop
- flutter test --coverage
- genhtml coverage/lcov.info -o coverage/html
# Todo: gonna need more work on container
# https://flutter.dev/desktop
# requirements: Visual Studio 2019 (not to be confused with Visual Studio Code) with the “Desktop development with C++” workload installed, including all of its default components
#- name: build-windows
# image: cirrusci/flutter:dev
#- volumes:
# - name: deps
# path: /root/.pub-cache
# commands:
# - flutter config --enable-windows-desktop
# - flutter build windows
- name: deploy-buildfiles
image: kroniak/ssh-client
environment:
BUILDFILES_KEY:
from_secret: buildfiles_key
secrets: [gogs_account_token]
when:
event: push
status: [ success ]
commands:
- echo $BUILDFILES_KEY > ~/id_rsab64
- base64 -d ~/id_rsab64 > ~/id_rsa
- chmod 400 ~/id_rsa
- export DIR=flwtch-`cat VERSION`-`cat BUILDDATE`
- mv deploy $DIR
- cp -r coverage/html $DIR/coverage-tests
- cp -r test/failures $DIR/test-failures || true
- cd $DIR
- find . -type f -exec sha256sum {} \; > ./../sha256s.txt
- mv ./../sha256s.txt .
- cd ..
# TODO: do deployment once files actaully compile
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
- name: notify-email
image: drillster/drone-email
settings:
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
status: [ failure ]
- name: notify-gogs
image: openpriv/drone-gogs
when:
event: pull_request
status: [ success, changed, failure ]
environment:
GOGS_ACCOUNT_TOKEN:
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
- name: deps
temp: {}
trigger:
repo: flutter/flutter_app
branch: trunk
event:
- push
- pull_request
---
kind: pipeline kind: pipeline
type: docker type: docker
name: windows name: windows
@ -12,7 +179,7 @@ clone:
steps: steps:
- name: clone - name: clone
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc image: openpriv/flutter-desktop:windows-sdk30-fdev2.2rc
environment: environment:
buildbot_key_b64: buildbot_key_b64:
from_secret: buildbot_key_b64 from_secret: buildbot_key_b64
@ -30,70 +197,43 @@ steps:
- git checkout $DRONE_COMMIT - git checkout $DRONE_COMMIT
- name: fetch - name: fetch
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc image: openpriv/flutter-desktop:windows-sdk30-fdev2.2rc
commands: commands:
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip" - powershell -command "Invoke-WebRequest -Uri https://www.torproject.org/dist/torbrowser/10.0.16/tor-win32-0.4.5.7.zip -OutFile tor.zip"
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' }" - powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '2b7d683f036d0fec149f1d2bdfcf5b7ef4c337005a2b685c056b00047fdb2b57d4c25b8559ad7ef5c7a030b273934be82a9f83ef6e391f5d7d13d8d6c83e8048' ) { Write-Error 'tor.zip sha512sum mismatch' }"
- git describe --tags > VERSION - git describe --tags > VERSION
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE - powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
- .\fetch-libcwtch-go.ps1 - .\fetch-libcwtch-go.ps1
-
- name: build-windows - name: build-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc image: openpriv/flutter-desktop:windows-beta
commands: commands:
- flutter pub get - flutter pub get
- $Env:version += type .\VERSION - flutter build windows
- $Env:builddate += type .\BUILDDATE # flwtch-`cat VERSION`-`cat BUILDDATE`
- $Env:releasedir = "build\\windows\\runner\\Release\\" - $Env:buildname = 'flwtch-win-'
- flutter build windows --dart-define BUILD_VER=$Env:version --dart-define BUILD_DATE=$Env:builddate - $Env:buildname += type .\VERSION
- copy windows\libCwtch.dll $Env:releasedir - $Env:buildname += '-'
# flutter hasn't worked out it's packaging of required dll's so we have to resort to this manual nonsense - $Env:buildname += type .\BUILDDATE
# https://github.com/google/flutter-desktop-embedding/issues/587 - $Env:builddir += $Env:buildname
# https://github.com/flutter/flutter/issues/53167 - $Env:zip = 'flwtch.zip'
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir - $Env:sha = $Env:zip
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir - $Env:sha += '.sha512'
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:releasedir\Tor"
- name: package-windows
image: openpriv/nsis:latest
environment:
pfx:
from_secret: pfx
pfx_pass:
from_secret: pfx_pass
commands:
- $Env:version += type .\VERSION
- $Env:builddate += type .\BUILDDATE
- $Env:releasedir = "build\\windows\\runner\\Release\\"
- $Env:zip = 'cwtch-' + $Env:version + '.zip'
- $Env:zipsha = $Env:zip + '.sha512'
- $Env:msix = 'cwtch-install-' + $Env:version + '.msix'
- $Env:msixsha = $Env:msix + '.sha512'
- $Env:buildname = 'flwtch-win-' + $Env:version + '-' + $Env:builddate
- $Env:builddir = $Env:buildname
- echo $Env:pfx > codesign.pfx.b64
- certutil -decode codesign.pfx.b64 codesign.pfx
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com $Env:releasedir\cwtch.exe
- copy windows\runner\resources\knot_128.ico $Env:releasedir\cwtch.ico
- makensis windows\nsis\cwtch-installer.nsi
- move windows\nsis\cwtch-installer.exe cwtch-installer.exe
- signtool sign /v /fd sha256 /a /f codesign.pfx /p $Env:pfx_pass /tr http://timestamp.digicert.com cwtch-installer.exe
- powershell -command "(Get-FileHash cwtch-installer.exe -Algorithm sha512).Hash" > cwtch-installer.sha512
- mkdir deploy - mkdir deploy
- move build\\windows\\runner\\Release $Env:builddir
- copy windows\libCwtch.dll $Env:builddir
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:builddir\Tor"
- powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath $Env:zip"
- powershell -command "(Get-FileHash *.zip -Algorithm sha512).Hash" > $Env:sha
- mkdir deploy\$Env:builddir - mkdir deploy\$Env:builddir
- move $Env:releasedir $Env:builddir - move $Env:zip deploy\$Env:builddir
- powershell -command "Compress-Archive -Path $Env:builddir -DestinationPath cwtch.zip" - move $Env:sha deploy\$Env:builddir
- powershell -command "(Get-FileHash cwtch.zip -Algorithm sha512).Hash" > $Env:zipsha
- move cwtch-installer.exe deploy\$Env:builddir\cwtch-installer.exe
- move cwtch.zip deploy\$Env:builddir\$Env:zip
- move *.sha512 deploy\$Env:builddir
- name: deploy-windows - name: deploy-windows
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc image: openpriv/flutter-desktop:windows-sdk30-fdev2.2rc
when: when:
event: push event: push
status: [ success ] status: [ success ]
environment: environment:
BUILDFILES_KEY: BUILDFILES_KEY:
from_secret: buildfiles_key from_secret: buildfiles_key
@ -106,4 +246,4 @@ trigger:
repo: flutter/flutter_app repo: flutter/flutter_app
branch: trunk branch: trunk
event: event:
- push - push

View File

@ -1 +1 @@
v0.0.2-108-g3964348-2021-06-24-17-42 v0.0.2-36-g84d85b7-2021-05-20-01-14

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2021 Open Privacy Research Society
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,6 +1,6 @@
# Cwtch UI # flwtch
A Flutter based Cwtch UI A new Flutter application.
## Getting Started ## Getting Started
@ -8,19 +8,13 @@ click the play button in android studio
### Linux ### Linux
- libCwtch-go: required to be on the link path (linux/cwtch.destktop demonstrates with `env LD_LIBRARY_PATH=./lib/` on the front of the comman) - libCwtch-go: the result of `make linux`, `libCwtch.so` should be in the link path
- fetch-libcwtch-go.sh will fetch a prebuilt version
- or compile from source from libcwtch-go with `make linux`
- `tor` should be in the PATH - `tor` should be in the PATH
### Windows ### Windows
- run `fetch-libcwtch-go.ps1` to get `libCwtch.dll` which is required to run - libCwtch-go: the result of `make windows`, `libCwtch.dll` should be placed in the source root
- run `fetch-tor-win.ps1` to fetch Tor for windows - tor is bundled in `windors/Tor`
#### Issues
- Flutter engine has a [known bug](https://github.com/flutter/flutter/issues/75675) around the Right Shift key being sticky. We have implemented the mostly work around, but until it is fixed, right shift occasionally acts permenent. If this happens, just tap left shift and it will reset
## l10n ## l10n
@ -32,24 +26,14 @@ After adding a new key and providing/obtaining translations for it, follow the n
### Updating translations ### Updating translations
Only Open Privacy staff members can update translations. Only Open Privacy staff members can update translations automatically:
In Lokalise, hit Download and make sure: ```
flutter pub run flutter_lokalise download -v --api-token "<X>" --project-id "<Y>"
```
* Format is set to "Flutter (.arb) This will download a bundle of translations from Lokalise and convert it to resource files in `lib/l10n/intl_*.arb`.
* Output filename is set to `l10n/intl_%LANG_ISO%.%FORMAT%` The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).
* Empty translations is set to "Replace with base language"
Build, download and unzip the output, overwriting `lib/l10n`. The next time Flwtch is built, Flutter will notice the changes and update `app_localizations.dart` accordingly (thanks to `generate:true` in `pubspec.yaml`).
### Adding a language
If a new language has been added to the Lokalise project, two additional manual steps need to be done:
* Create a new key called `localeXX` for the name of the language
* Add it to the settings pane by updating `getLanguageFull()` in `lib/views/globalsettingsview.dart`
Then rebuild as normal.
### Using a string ### Using a string
@ -62,10 +46,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
and then use: and then use:
``` ```
Text(AppLocalizations.of(context)!.stringIdentifer), Text(AppLocalizations.of(context).stringIdentifer),
``` ```
### Configuration ### Configuration
With `generate: true` in `pubspec.yaml`, the Flutter build process checks `l10n.yaml` for input/output filenames. API tokens are only available to Open Privacy staff at this time, who will perform the translation updates for you as part of merging your PRs.
With `generate: true` in `pubspec.yaml`, the Flutter build process checks `l10n.yaml` for input/output filenames.

50
SPEC.md
View File

@ -8,7 +8,7 @@ required - any new Cwtch work is beyond the scope of this initial spec.
# Functional Requirements # Functional Requirements
- [ ] Kill all processes / isolates on exit (Blocked - P1) - [ ] Kill all processes / isolates on exit (Blocked - P1)
- [X] Android Service? (P1) - [ ] Android Service? (P1)
# Splash Screen # Splash Screen
- [X] Android - [X] Android
@ -16,9 +16,9 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] Desktop (P2) - [ ] Desktop (P2)
# Custom Styled Widgets # Custom Styled Widgets
- [X] Label Widget - [/] Label Widget
- [X] Initial - [X] Initial
- [X] With Accessibility / Zoom Integration (P1) - [ ] With Accessibility / Zoom Integration (P1)
- [X] Text Field Widget - [X] Text Field Widget
- [X] Password Widget - [X] Password Widget
- [X] Text Button Widget (for Copy) - [X] Text Button Widget (for Copy)
@ -33,10 +33,10 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Profile Picture - [X] Profile Picture
- [X] default images - [X] default images
- [ ] custom images (P3) - [ ] custom images (P3)
- [X] coloured ring border (P2) - [ ] coloured ring border (P2)
- [X] Profile Name - [X] Profile Name
- [X] Edit Button - [X] Edit Button
- [X Unread messages badge (P2) - [ ] Unread messages badge (P2)
- [X] Navigate to a specific Profile Contacts Pane (when clicking on a Profile row) - [X] Navigate to a specific Profile Contacts Pane (when clicking on a Profile row)
- [X] Navigate to a specific Profile Management Pane (edit Button) - [X] Navigate to a specific Profile Management Pane (edit Button)
- [X] Navigate to the Settings Pane (Settings Button in Action bar) - [X] Navigate to the Settings Pane (Settings Button in Action bar)
@ -54,12 +54,12 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Update Profile Name - [X] Update Profile Name
- [X] Update Profile Password - [X] Update Profile Password
- [X] Error Message When Attempting to Update Password with Wrong Old Password (P2) - [ ] Error Message When Attempting to Update Password with Wrong Old Password (P2)
- [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile (P3) - [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile (P3)
- [X] Delete a Profile (P2) - [ ] Delete a Profile (P2)
- [X] Dialog Acknowledgement (P2) - [ ] Dialog Acknowledgement (P2)
- [X] Require Old Password Gate (P2) - [ ] Require Old Password Gate (P2)
- [X] Async Checking of Password (P2) - [ ] Async Checking of Password (P2)
- [X] Copy Profile Onion Address - [X] Copy Profile Onion Address
## Profile Pane (formally Contacts Pane) ## Profile Pane (formally Contacts Pane)
@ -76,11 +76,11 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Name - [X] Name
- [X] Onion - [X] Onion
- [X] Online Status - [X] Online Status
- [X] Unread Messages Badge (P1) - [ ] Unread Messages Badge (P1)
- [X] In Order of Most Recent Message / Activity (P1) - [ ] In Order of Most Recent Message / Activity (P1)
- [X] With Accept / Reject Heart/Trash Bin Option (P1) - [ ] With Accept / Reject Heart/Trash Bin Option (P1)
- [X] Separate list area for Blocked Contacts (P1) - [ ] Separate list area for Blocked Contacts (P1)
- [X] Display all Group Contacts (if experiment is enabled) - [ ] Display all Group Contacts (if experiment is enabled)
- [X] Navigate to a specific Contact or Group Message Pane (Contact Row) - [X] Navigate to a specific Contact or Group Message Pane (Contact Row)
- [X] Pressing Back should go back to the home pane - [X] Pressing Back should go back to the home pane
@ -88,22 +88,22 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Allowing Copying the Profile Onion Address for Sharing - [X] Allowing Copying the Profile Onion Address for Sharing
- [X] Allowing Pasting a Peer Onion Address for adding to Contacts - [X] Allowing Pasting a Peer Onion Address for adding to Contacts
- [ ] (with optional name field) - [ ] (with optional name field)
- [X] Allowing Pasting a Group Invite / Server Address - [ ] Allowing Pasting a Group Invite / Server Address
- [X] (if group experiment is enabled) - [X] (if group experiment is enabled)
## Message Overlay ## Message Overlay
- [X] Display Messages from Contacts - [X] Display Messages from Contacts
- [X] Allowing copying the text of a specific message (on mobile) (P2) - [ ] Allowing copying the text of a specific message (on mobile) (P2)
- [X] Send a message to the specific Contact / Group - [X] Send a message to the specific Contact / Group
- [~] Display the Acknowledgement status of a message (P1) - [~] Display the Acknowledgement status of a message (P1)
- [X] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar) - [ ] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar)
- [ ] Emoji Support (P1) - [ ] Emoji Support (P1)
- [ ] Display in-message emoji text labels e.g. `:label:` as emoji. (P1) - [ ] Display in-message emoji text labels e.g. `:label:` as emoji. (P1)
- [ ] Functional Emoji Drawer Widget for Selection (P2) - [ ] Functional Emoji Drawer Widget for Selection (P2)
- [ ] Mutant Standard? (P2) - [ ] Mutant Standard? (P2)
- [X] Display a warning if Contact / Server is offline (Broken Heart) (P1) - [ ] Display a warning if Contact / Server is offline (Broken Heart) (P1)
- [X] Display a warning for configuring peer history (P2) - [ ] Display a warning for configuring peer history (P2)
- [X] Pressing Back should go back to the contacts pane - [X] Pressing Back should go back to the contacts pane
## List Overlay (P3) ## List Overlay (P3)
@ -123,11 +123,11 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Pressing Back should go back to the message pane - [X] Pressing Back should go back to the message pane
## Group Settings Pane (experimental - P3) ## Group Settings Pane (experimental - P3)
- [X] Gated behind group experiment - [ ] Gated behind group experiment
- [X] Update local name of group - [ ] Update local name of group
- [X] Get Group Invite - [ ] Get Group Invite
- [X] Leave Group - [ ] Leave Group
- [X] Pressing Back should go back to the message pane for the group - [ ] Pressing Back should go back to the message pane for the group

View File

@ -69,15 +69,6 @@ android {
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
} }
flutter { flutter {
@ -91,30 +82,4 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "com.airbnb.android:lottie:3.5.0" implementation "com.airbnb.android:lottie:3.5.0"
implementation "com.android.support.constraint:constraint-layout:2.0.4" implementation "com.android.support.constraint:constraint-layout:2.0.4"
// WorkManager
// (Java only)
//implementation("androidx.work:work-runtime:$work_version")
// Kotlin + coroutines
implementation("androidx.work:work-runtime-ktx:2.5.0")
// optional - RxJava2 support
//implementation("androidx.work:work-rxjava2:$work_version")
// optional - GCMNetworkManager support
//implementation("androidx.work:work-gcm:$work_version")
// optional - Test helpers
//androidTestImplementation("androidx.work:work-testing:$work_version")
// optional - Multiprocess support
implementation "androidx.work:work-multiprocess:2.5.0"
// end of workmanager deps
// needed to prevent a ListenableFuture dependency conflict/bug
// see https://github.com/google/ExoPlayer/issues/7905#issuecomment-692637059
implementation 'com.google.guava:guava:any'
} }

View File

@ -8,7 +8,6 @@
<application <application
android:name="io.flutter.app.FlutterApplication" android:name="io.flutter.app.FlutterApplication"
android:label="Cwtch" android:label="Cwtch"
android:extractNativeLibs="true"
android:icon="@mipmap/knott"> android:icon="@mipmap/knott">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -43,9 +42,4 @@
<!--Needed to access Tor socket--> <!--Needed to access Tor socket-->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!--Needed to run in background (lol)-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
<uses-permission android:name="android.permission.GET_TASKS" />
</manifest> </manifest>

View File

@ -1,266 +0,0 @@
package im.cwtch.flwtch
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.*
import cwtch.Cwtch
import io.flutter.FlutterInjector
import org.json.JSONObject
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
private var notificationID: MutableMap<String, Int> = mutableMapOf()
private var notificationIDnext: Int = 1
override suspend fun doWork(): Result {
val method = inputData.getString(KEY_METHOD)
?: return Result.failure()
val args = inputData.getString(KEY_ARGS)
?: return Result.failure()
// Mark the Worker as important
val progress = "Cwtch is keeping Tor running in the background"//todo:translate
setForeground(createForegroundInfo(progress))
return handleCwtch(method, args)
}
private fun getNotificationID(profile: String, contact: String): Int {
val k = "$profile $contact"
if (!notificationID.containsKey(k)) {
notificationID[k] = notificationIDnext++
}
return notificationID[k] ?: -1
}
private fun handleCwtch(method: String, args: String): Result {
val a = JSONObject(args)
when (method) {
"Start" -> {
Log.i("FlwtchWorker.kt", "handleAppInfo Start")
val appDir = (a.get("appDir") as? String) ?: ""
val torPath = (a.get("torPath") as? String) ?: "tor"
Log.i("FlwtchWorker.kt", "appDir: '$appDir' torPath: '$torPath'")
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
while(true) {
Log.i("FlwtchWorker.kt", "while(true)getAppbusEvent()")
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
val data = JSONObject(evt.Data)
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createMessageNotificationChannel(data.getString("RemotePeer"), data.getString("RemotePeer"))
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val loader = FlutterInjector.instance().flutterLoader()
val key = loader.getLookupKeyForAsset("assets/"+data.getString("Picture"))//"assets/profiles/001-centaur.png")
val fh = applicationContext.assets.open(key)
val clickIntent = Intent(applicationContext, MainActivity::class.java).also { intent ->
intent.action = Intent.ACTION_RUN
intent.putExtra("EventType", "NotificationClicked")
intent.putExtra("ProfileOnion", data.getString("ProfileOnion"))
intent.putExtra("RemotePeer", if (evt.EventType == "NewMessageFromPeer") data.getString("RemotePeer") else data.getString("GroupID"))
}
val newNotification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(data.getString("Nick"))
.setContentText("New message")//todo: translate
.setLargeIcon(BitmapFactory.decodeStream(fh))
.setSmallIcon(R.mipmap.knott)
.setContentIntent(PendingIntent.getActivity(applicationContext, 1, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.build()
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), data.getString("RemotePeer")), newNotification)
}
Intent().also { intent ->
intent.action = "im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS"
intent.putExtra("EventType", evt.EventType)
intent.putExtra("Data", evt.Data)
intent.putExtra("EventID", evt.EventID)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
}
}
}
"ReconnectCwtchForeground" -> {
Cwtch.reconnectCwtchForeground()
}
"CreateProfile" -> {
val nick = (a.get("nick") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass = (a.get("pass") as? String) ?: ""
Cwtch.loadProfiles(pass)
}
"GetMessage" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val indexI = a.getInt("index")
Log.i("FlwtchWorker.kt", "indexI = $indexI")
return Result.success(Data.Builder().putString("result", Cwtch.getMessage(profile, handle, indexI.toLong())).build())
}
"UpdateMessageFlags" -> {
val profile = (a.get("profile") as? String) ?: ""
val handle = (a.get("contact") as? String) ?: ""
val midx = (a.get("midx") as? Long) ?: 0
val flags = (a.get("flags") as? Long) ?: 0
Cwtch.updateMessageFlags(profile, handle, midx, flags)
}
"AcceptContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.acceptContact(profile, handle)
}
"BlockContact" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
Cwtch.blockContact(profile, handle)
}
"SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val message = (a.get("message") as? String) ?: ""
Cwtch.sendMessage(profile, handle, message)
}
"SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val handle = (a.get("handle") as? String) ?: ""
val target = (a.get("target") as? String) ?: ""
Cwtch.sendInvitation(profile, handle, target)
}
"SendProfileEvent" -> {
val onion = (a.get("onion") as? String) ?: ""
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendProfileEvent(onion, jsonEvent)
}
"SendAppEvent" -> {
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
Cwtch.sendAppEvent(jsonEvent)
}
"ResetTor" -> {
Cwtch.resetTor()
}
"ImportBundle" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val bundle = (a.get("bundle") as? String) ?: ""
Cwtch.importBundle(profile, bundle)
}
"SetGroupAttribute" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
val key = (a.get("key") as? String) ?: ""
val value = (a.get("value") as? String) ?: ""
Cwtch.setGroupAttribute(profile, groupHandle, key, value)
}
"CreateGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val server = (a.get("server") as? String) ?: ""
val groupName = (a.get("groupname") as? String) ?: ""
Cwtch.createGroup(profile, server, groupName)
}
"DeleteProfile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val pass = (a.get("pass") as? String) ?: ""
Cwtch.deleteProfile(profile, pass)
}
"LeaveConversation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val contactHandle = (a.get("contactHandle") as? String) ?: ""
Cwtch.leaveConversation(profile, contactHandle)
}
"LeaveGroup" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.leaveGroup(profile, groupHandle)
}
"RejectInvite" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val groupHandle = (a.get("groupHandle") as? String) ?: ""
Cwtch.rejectInvite(profile, groupHandle)
}
"Shutdown" -> {
Cwtch.shutdownCwtch();
return Result.success()
}
else -> return Result.failure()
}
return Result.success()
}
// Creates an instance of ForegroundInfo which can be used to update the
// ongoing notification.
private fun createForegroundInfo(progress: String): ForegroundInfo {
val id = "flwtch"
val title = "Flwtch"
val cancel = "Shut down"//todo: translate
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createForegroundNotificationChannel(id, id)
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
// This PendingIntent can be used to cancel the worker
val intent = WorkManager.getInstance(applicationContext)
.createCancelPendingIntent(getId())
val notification = NotificationCompat.Builder(applicationContext, channelId)
.setContentTitle(title)
.setTicker(title)
.setContentText(progress)
.setSmallIcon(R.mipmap.knott)
.setOngoing(true)
// Add the cancel action to the notification which can
// be used to cancel the worker
.addAction(android.R.drawable.ic_delete, cancel, intent)
.build()
return ForegroundInfo(101, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createForegroundNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
chan.lightColor = Color.MAGENTA
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(chan)
return channelId
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createMessageNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
chan.lightColor = Color.MAGENTA
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
notificationManager.createNotificationChannel(chan)
return channelId
}
companion object {
const val KEY_METHOD = "KEY_METHOD"
const val KEY_ARGS = "KEY_ARGS"
}
}

View File

@ -1,29 +1,32 @@
package im.cwtch.flwtch package im.cwtch.flwtch
import SplashView import SplashView
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.annotation.NonNull import androidx.annotation.NonNull
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.Window
import androidx.lifecycle.Observer import kotlinx.coroutines.Dispatchers
import androidx.localbroadcastmanager.content.LocalBroadcastManager import kotlinx.coroutines.GlobalScope
import androidx.work.* import kotlinx.coroutines.launch
import io.flutter.embedding.android.SplashScreen import io.flutter.embedding.android.SplashScreen
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import cwtch.Cwtch
import io.flutter.plugin.common.EventChannel
import kotlin.concurrent.thread
import org.json.JSONObject import org.json.JSONObject
import java.util.concurrent.TimeUnit
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView() override fun provideSplashScreen(): SplashScreen? = SplashView()
// Channel to get app info // Channel to get app info
@ -36,37 +39,13 @@ class MainActivity: FlutterActivity() {
// Channel to send eventbus events on // Channel to send eventbus events on
private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus" private val CWTCH_EVENTBUS = "test.flutter.dev/eventBus"
// Channel to trigger contactview when an external notification is clicked
private val CHANNEL_NOTIF_CLICK = "im.cwtch.flwtch/notificationClickHandler"
// WorkManager tag applied to all Start() infinite coroutines
val WORKER_TAG = "cwtchEventBusWorker"
private var myReceiver: MyBroadcastReceiver? = null
private var methodChan: MethodChannel? = null
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (methodChan == null || intent.extras == null) return
if (!intent.extras!!.containsKey("ProfileOnion") || !intent.extras!!.containsKey("RemotePeer")) {
Log.i("onNewIntent", "got intent with no onions")
return
}
val profile = intent.extras!!.getString("ProfileOnion")
val handle = intent.extras!!.getString("RemotePeer")
val mappo = mapOf("ProfileOnion" to profile, "RemotePeer" to handle)
val j = JSONObject(mappo)
methodChan!!.invokeMethod("NotificationClicked", j.toString())
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// Note: this methods are invoked on the main thread. // Note: this methods are invoked on the main thread.
//note to self: ask someone if this does anything ^ea
requestWindowFeature(Window.FEATURE_NO_TITLE)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_APP_INFO).setMethodCallHandler { call, result -> handleAppInfo(call, result) }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) } MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_CWTCH).setMethodCallHandler { call, result -> handleCwtch(call, result) }
methodChan = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NOTIF_CLICK)
} }
private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) { private fun handleAppInfo(@NonNull call: MethodCall, @NonNull result: Result) {
@ -84,88 +63,128 @@ class MainActivity: FlutterActivity() {
return ainfo.nativeLibraryDir return ainfo.nativeLibraryDir
} }
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
var method = call.method when (call.method) {
val argmap: Map<String, String> = call.arguments as Map<String, String> "Start" -> {
Log.i("MainActivity.kt", "handleAppInfo Start")
val appDir = (call.argument("appDir") as? String) ?: "";
val torPath = (call.argument("torPath") as? String) ?: "tor";
Log.i("MainActivity.kt", " appDir: '" + appDir + "' torPath: '" + torPath + "'")
Cwtch.startCwtch(appDir, torPath)
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start() // seperate coroutine to poll event bus and send to dart
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register val eventbus_chan = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
// the eventbus listener. Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...")
if (call.method == "Start") { GlobalScope.launch(Dispatchers.IO) {
val uniqueTag = argmap["torPath"] ?: "nullEventBus" while(true) {
val evt = AppbusEvent(Cwtch.getAppBusEvent())
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get Log.i("MainActivity.kt", "got appbusEvent: " + evt)
// accidentally duplicated. however, we still need to manually check if it's running or not, so launch(Dispatchers.Main) {
// that we can divert this method call to ReconnectCwtchForeground instead if so. //todo: this elides evt.EventID which may be needed at some point?
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get() eventbus_chan.invokeMethod(evt.EventType, evt.Data)
for (workInfo in works) { }
Log.i("handleCwtch:WorkManager", "$workInfo") }
if (!workInfo.tags.contains(uniqueTag)) {
Log.i("handleCwtch:WorkManager", "canceling ${workInfo.id} bc tags don't include $uniqueTag")
WorkManager.getInstance(this).cancelWorkById(workInfo.id)
} }
} }
WorkManager.getInstance(this).pruneWork() "SelectProfile" -> {
val onion = (call.argument("profile") as? String) ?: "";
Log.i("MainActivity.kt", "Start() launching foregroundservice") Cwtch.selectProfile(onion)
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
return
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
}
} }
) "CreateProfile" -> {
} val nick = (call.argument("nick") as? String) ?: "";
val pass = (call.argument("pass") as? String) ?: "";
Cwtch.createProfile(nick, pass)
}
"LoadProfiles" -> {
val pass = (call.argument("pass") as? String) ?: "";
Cwtch.loadProfiles(pass)
}
"GetProfiles" -> result.success(Cwtch.getProfiles())
// "ACNEvents" -> result.success(Cwtch.acnEvents())
"ContactEvents" -> result.success(Cwtch.contactEvents())
"NumMessages" -> {
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
result.success(Cwtch.numMessages(profile, handle))
}
"GetMessage" -> {
//Log.i("MainActivivity.kt", (call.argument("index")));
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver // var args : HashMap<String, dynamic> = call.arguments();
override fun onResume() { // Log.i("MainActivity.kt", args);
super.onResume()
Log.i("MainActivity.kt", "onResume")
if (myReceiver == null) { val profile = (call.argument("profile") as? String) ?: "";
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder") val handle = (call.argument("contact") as? String) ?: "";
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS) val indexI = call.argument<Int>("index") ?: 0;
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS") Log.i("MainActivity.kt", "indexI = " + indexI)
myReceiver = MyBroadcastReceiver(mc) result.success(Cwtch.getMessage(profile, handle, indexI.toLong()))
LocalBroadcastManager.getInstance(applicationContext).registerReceiver(myReceiver!!, filter) }
"GetMessages" -> {
val profile = (call.argument("profile") as? String) ?: "";
val handle = (call.argument("contact") as? String) ?: "";
val start = (call.argument("start") as? Long) ?: 0;
val end = (call.argument("end") as? Long) ?: 0;
result.success(Cwtch.getMessages(profile, handle, start, end))
}
"AcceptContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.acceptContact(profile, handle);
}
"BlockContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.blockContact(profile, handle);
}
"DebugResetContact" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
Cwtch.debugResetContact(profile, handle);
}
"SendMessage" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
val message = (call.argument("message") as? String) ?: "";
Cwtch.sendMessage(profile, handle, message);
}
"SendInvitation" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val handle = (call.argument("handle") as? String) ?: "";
val target = (call.argument("target") as? String) ?: "";
Cwtch.sendInvitation(profile, handle, target);
}
"SendProfileEvent" -> {
val onion = (call.argument("onion") as? String) ?: "";
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendProfileEvent(onion, jsonEvent);
}
"SendAppEvent" -> {
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendAppEvent(jsonEvent);
}
"ResetTor" -> {
Cwtch.resetTor();
}
"ImportBundle" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val bundle = (call.argument("bundle") as? String) ?: "";
Cwtch.importBundle(profile, bundle);
}
"SetGroupAttribute" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") as? String) ?: "";
val key = (call.argument("key") as? String) ?: "";
val value = (call.argument("value") as? String) ?: "";
Cwtch.setGroupAttribute(profile, groupHandle, key, value);
}
"RejectInvite" -> {
val profile = (call.argument("ProfileOnion") as? String) ?: "";
val groupHandle = (call.argument("groupHandle") as? String) ?: "";
Cwtch.rejectInvite(profile, groupHandle);
}
else -> result.notImplemented()
} }
// ReconnectCwtchForeground which will resync counters and settings...
// We need to do this here because after a "pause" flutter is still running
// but we might have lost sync with the background process...
Log.i("MainActivity.kt", "Call ReconnectCwtchForeground")
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, "ReconnectCwtchForeground").putString(FlwtchWorker.KEY_ARGS, "{}").build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(applicationContext).enqueue(workRequest)
}
override fun onStop() {
super.onStop()
Log.i("MainActivity.kt", "onStop")
if (myReceiver != null) {
LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(myReceiver!!);
myReceiver = null;
}
}
override fun onDestroy() {
super.onDestroy()
Log.i("MainActivity.kt", "onDestroy - cancelling all WORKER_TAG and pruning old work")
WorkManager.getInstance(this).cancelAllWorkByTag(WORKER_TAG)
WorkManager.getInstance(this).pruneWork()
} }
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095 // source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
@ -187,17 +206,4 @@ class MainActivity: FlutterActivity() {
val EventID = this.optString("EventID") val EventID = this.optString("EventID")
val Data = this.optString("Data") val Data = this.optString("Data")
} }
// MainActivity.MyBroadcastReceiver receives events from the Cwtch service via im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS Android local broadcast intents
// then it forwards them to the flutter ui engine using the CWTCH_EVENTBUS methodchannel
class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() {
val eventBus: MethodChannel = mc
override fun onReceive(context: Context, intent: Intent) {
val evtType = intent.getStringExtra("EventType") ?: ""
val evtData = intent.getStringExtra("Data") ?: ""
//val evtID = intent.getStringExtra("EventID") ?: ""//todo?
eventBus.invokeMethod(evtType, evtData)
}
}
} }

Binary file not shown.

Binary file not shown.

View File

@ -11,7 +11,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:lottie_autoPlay="true" app:lottie_autoPlay="true"
app:lottie_rawRes="@raw/cwtch_animated_logo_op" app:lottie_rawRes="@raw/cwtch_animated_logo"
app:lottie_loop="true" app:lottie_loop="true"
app:lottie_speed="1.00" /> app:lottie_speed="1.00" />

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
//removed due to gradle namespace conflicts that are beyond erinn's mere mortal understanding task clean(type: Delete) {
//task clean(type: Delete) { delete rootProject.buildDir
// delete rootProject.buildDir }
//}

View File

@ -2,4 +2,3 @@ org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.enableR8=true android.enableR8=true
android.bundle.enableUncompressedNativeLibs=false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,399 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;"
xml:space="preserve"
sodipodi:docname="Cwtch_knott_white.svg"
inkscape:export-filename="/home/sarah/PARA/projects/cwtch/assets/core/knott-white.png"
inkscape:export-xdpi="98.300003"
inkscape:export-ydpi="98.300003"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata35"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs33" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview31"
showgrid="false"
inkscape:zoom="1.3350176"
inkscape:cx="-56.414859"
inkscape:cy="254.41396"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#FFFFFF;}
.st1{fill:#010101;}
.st2{fill:#AF9CBA;}
.st3{clip-path:url(#SVGID_2_);}
.st4{clip-path:url(#SVGID_3_);}
.st5{clip-path:url(#SVGID_4_);}
.st6{clip-path:url(#SVGID_6_);}
.st7{clip-path:url(#SVGID_7_);}
.st8{clip-path:url(#SVGID_8_);}
.st9{clip-path:url(#SVGID_10_);}
.st10{clip-path:url(#SVGID_11_);}
.st11{clip-path:url(#SVGID_12_);}
.st12{clip-path:url(#SVGID_14_);}
.st13{clip-path:url(#SVGID_15_);}
.st14{clip-path:url(#SVGID_16_);}
.st15{clip-path:url(#SVGID_18_);}
.st16{clip-path:url(#SVGID_19_);}
.st17{clip-path:url(#SVGID_20_);}
.st18{clip-path:url(#SVGID_22_);}
.st19{clip-path:url(#SVGID_23_);}
.st20{clip-path:url(#SVGID_24_);}
.st21{clip-path:url(#SVGID_26_);}
.st22{clip-path:url(#SVGID_27_);}
.st23{clip-path:url(#SVGID_28_);}
.st24{clip-path:url(#SVGID_30_);}
.st25{clip-path:url(#SVGID_31_);}
.st26{clip-path:url(#SVGID_32_);}
.st27{clip-path:url(#SVGID_34_);}
.st28{clip-path:url(#SVGID_35_);}
.st29{clip-path:url(#SVGID_36_);}
.st30{clip-path:url(#SVGID_38_);}
.st31{clip-path:url(#SVGID_39_);}
.st32{clip-path:url(#SVGID_40_);}
.st33{clip-path:url(#SVGID_42_);}
.st34{clip-path:url(#SVGID_43_);}
.st35{clip-path:url(#SVGID_44_);}
.st36{clip-path:url(#SVGID_46_);}
.st37{clip-path:url(#SVGID_47_);}
.st38{clip-path:url(#SVGID_48_);}
.st39{clip-path:url(#SVGID_50_);}
.st40{clip-path:url(#SVGID_51_);}
.st41{clip-path:url(#SVGID_52_);}
.st42{clip-path:url(#SVGID_54_);}
.st43{clip-path:url(#SVGID_55_);}
.st44{clip-path:url(#SVGID_56_);}
.st45{clip-path:url(#SVGID_58_);}
.st46{clip-path:url(#SVGID_59_);}
.st47{clip-path:url(#SVGID_60_);}
.st48{clip-path:url(#SVGID_62_);}
.st49{clip-path:url(#SVGID_63_);}
.st50{clip-path:url(#SVGID_64_);}
.st51{clip-path:url(#SVGID_66_);}
.st52{clip-path:url(#SVGID_67_);}
.st53{clip-path:url(#SVGID_68_);}
.st54{clip-path:url(#SVGID_70_);}
.st55{clip-path:url(#SVGID_71_);}
.st56{clip-path:url(#SVGID_72_);}
.st57{clip-path:url(#SVGID_74_);}
.st58{clip-path:url(#SVGID_75_);}
.st59{clip-path:url(#SVGID_76_);}
.st60{clip-path:url(#SVGID_78_);}
.st61{clip-path:url(#SVGID_79_);}
.st62{clip-path:url(#SVGID_80_);}
.st63{clip-path:url(#SVGID_82_);}
.st64{clip-path:url(#SVGID_83_);}
.st65{clip-path:url(#SVGID_84_);}
.st66{clip-path:url(#SVGID_86_);}
.st67{clip-path:url(#SVGID_87_);}
.st68{clip-path:url(#SVGID_88_);}
.st69{clip-path:url(#SVGID_90_);}
.st70{clip-path:url(#SVGID_91_);}
.st71{clip-path:url(#SVGID_92_);}
.st72{clip-path:url(#SVGID_94_);}
.st73{clip-path:url(#SVGID_95_);}
.st74{clip-path:url(#SVGID_96_);}
.st75{clip-path:url(#SVGID_98_);}
.st76{clip-path:url(#SVGID_99_);}
.st77{clip-path:url(#SVGID_100_);}
.st78{clip-path:url(#SVGID_102_);}
.st79{clip-path:url(#SVGID_103_);}
.st80{clip-path:url(#SVGID_104_);}
.st81{clip-path:url(#SVGID_106_);}
.st82{clip-path:url(#SVGID_107_);}
.st83{clip-path:url(#SVGID_108_);}
.st84{clip-path:url(#SVGID_110_);}
.st85{clip-path:url(#SVGID_111_);}
.st86{clip-path:url(#SVGID_112_);}
.st87{clip-path:url(#SVGID_114_);}
.st88{clip-path:url(#SVGID_115_);}
.st89{clip-path:url(#SVGID_116_);}
.st90{clip-path:url(#SVGID_118_);}
.st91{clip-path:url(#SVGID_119_);}
.st92{clip-path:url(#SVGID_120_);}
.st93{clip-path:url(#SVGID_122_);}
.st94{clip-path:url(#SVGID_123_);}
.st95{clip-path:url(#SVGID_124_);}
.st96{clip-path:url(#SVGID_126_);}
.st97{clip-path:url(#SVGID_127_);}
.st98{clip-path:url(#SVGID_128_);}
.st99{fill:#64317C;}
.st100{clip-path:url(#SVGID_130_);}
.st101{clip-path:url(#SVGID_131_);}
.st102{clip-path:url(#SVGID_132_);}
.st103{clip-path:url(#SVGID_134_);}
.st104{clip-path:url(#SVGID_135_);}
.st105{clip-path:url(#SVGID_136_);}
.st106{clip-path:url(#SVGID_138_);}
.st107{clip-path:url(#SVGID_139_);}
.st108{clip-path:url(#SVGID_140_);}
.st109{clip-path:url(#SVGID_142_);}
.st110{clip-path:url(#SVGID_143_);}
.st111{clip-path:url(#SVGID_144_);}
.st112{clip-path:url(#SVGID_146_);}
.st113{clip-path:url(#SVGID_147_);}
.st114{clip-path:url(#SVGID_148_);}
.st115{clip-path:url(#SVGID_150_);}
.st116{clip-path:url(#SVGID_151_);}
.st117{clip-path:url(#SVGID_152_);}
.st118{clip-path:url(#SVGID_154_);}
.st119{clip-path:url(#SVGID_155_);}
.st120{clip-path:url(#SVGID_156_);}
.st121{clip-path:url(#SVGID_158_);}
.st122{clip-path:url(#SVGID_159_);}
.st123{clip-path:url(#SVGID_160_);}
.st124{clip-path:url(#SVGID_162_);}
.st125{clip-path:url(#SVGID_163_);}
.st126{clip-path:url(#SVGID_164_);}
.st127{clip-path:url(#SVGID_166_);}
.st128{clip-path:url(#SVGID_167_);}
.st129{clip-path:url(#SVGID_168_);}
.st130{clip-path:url(#SVGID_170_);}
.st131{clip-path:url(#SVGID_171_);}
.st132{clip-path:url(#SVGID_172_);}
.st133{clip-path:url(#SVGID_174_);}
.st134{clip-path:url(#SVGID_175_);}
.st135{clip-path:url(#SVGID_176_);}
.st136{clip-path:url(#SVGID_178_);}
.st137{clip-path:url(#SVGID_179_);}
.st138{clip-path:url(#SVGID_180_);}
.st139{clip-path:url(#SVGID_182_);}
.st140{clip-path:url(#SVGID_183_);}
.st141{clip-path:url(#SVGID_184_);}
.st142{clip-path:url(#SVGID_186_);}
.st143{clip-path:url(#SVGID_187_);}
.st144{clip-path:url(#SVGID_188_);}
.st145{clip-path:url(#SVGID_190_);}
.st146{clip-path:url(#SVGID_191_);}
.st147{clip-path:url(#SVGID_192_);}
.st148{clip-path:url(#SVGID_194_);}
.st149{clip-path:url(#SVGID_195_);}
.st150{clip-path:url(#SVGID_196_);}
.st151{clip-path:url(#SVGID_198_);}
.st152{clip-path:url(#SVGID_199_);}
.st153{clip-path:url(#SVGID_200_);}
.st154{clip-path:url(#SVGID_202_);}
.st155{clip-path:url(#SVGID_203_);}
.st156{clip-path:url(#SVGID_204_);}
.st157{clip-path:url(#SVGID_206_);}
.st158{clip-path:url(#SVGID_207_);}
.st159{clip-path:url(#SVGID_208_);}
.st160{clip-path:url(#SVGID_210_);}
.st161{clip-path:url(#SVGID_211_);}
.st162{clip-path:url(#SVGID_212_);}
.st163{clip-path:url(#SVGID_214_);}
.st164{clip-path:url(#SVGID_215_);}
.st165{clip-path:url(#SVGID_216_);}
.st166{clip-path:url(#SVGID_218_);}
.st167{clip-path:url(#SVGID_219_);}
.st168{clip-path:url(#SVGID_220_);}
.st169{clip-path:url(#SVGID_222_);}
.st170{clip-path:url(#SVGID_223_);}
.st171{clip-path:url(#SVGID_224_);}
.st172{clip-path:url(#SVGID_226_);}
.st173{clip-path:url(#SVGID_227_);}
.st174{clip-path:url(#SVGID_228_);}
.st175{clip-path:url(#SVGID_230_);}
.st176{clip-path:url(#SVGID_231_);}
.st177{clip-path:url(#SVGID_232_);}
.st178{clip-path:url(#SVGID_234_);}
.st179{clip-path:url(#SVGID_235_);}
.st180{clip-path:url(#SVGID_236_);}
.st181{clip-path:url(#SVGID_238_);}
.st182{clip-path:url(#SVGID_239_);}
.st183{clip-path:url(#SVGID_240_);}
.st184{clip-path:url(#SVGID_242_);}
.st185{clip-path:url(#SVGID_243_);}
.st186{clip-path:url(#SVGID_244_);}
.st187{clip-path:url(#SVGID_246_);}
.st188{clip-path:url(#SVGID_247_);}
.st189{clip-path:url(#SVGID_248_);}
.st190{clip-path:url(#SVGID_250_);}
.st191{clip-path:url(#SVGID_251_);}
.st192{clip-path:url(#SVGID_252_);}
.st193{clip-path:url(#SVGID_254_);}
.st194{clip-path:url(#SVGID_255_);}
.st195{clip-path:url(#SVGID_256_);}
</style>
<g
transform="translate(2.1186441)"
id="g61"
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none">
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path37"
d="m 465.94,209.46 c -0.72,3.49 -2.35,7.45 -4.95,12.03 l 21.16,21.16 c 2.83,-5.51 7.14,-15.16 9.47,-26.61 4.76,-23.39 -0.6,-43.02 -15.93,-58.35 -28.01,-28.01 -57.26,-30.61 -94.84,-8.43 -9.04,5.34 -18.64,12.24 -28.55,20.51 -22.58,18.86 -47.88,45.85 -75.22,80.23 17.91,22.52 35,41.95 50.83,57.79 7.26,7.26 14.4,13.91 21.27,19.81 l 18.76,-18.76 c -6.83,-5.78 -13.98,-12.41 -21.32,-19.75 -10.39,-10.39 -21.5,-22.57 -32.99,-36.19 l -2.43,-2.9 2.44,-2.89 c 21.03,-24.91 40.59,-44.8 58.14,-59.09 11.14,-9.08 21.56,-15.98 30.98,-20.53 8.14,-3.92 15.55,-6.11 22.03,-6.49 3.16,-0.19 8.05,-0.15 14,2.3 5.84,2.4 11.79,6.68 18.2,13.09 2.39,2.39 5.82,6.25 7.91,12.08 2.16,6.02 2.51,12.85 1.07,20.88 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path39"
d="M 355.44,371.4 450,276.85 c -5.56,-8.23 -12.85,-16.22 -16.93,-20.49 l -93.37,93.37 c 6.02,7.42 11.29,14.68 15.74,21.67 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path41"
d="m 475.69,342.31 c 15.32,-15.32 20.68,-34.95 15.93,-58.34 -3.3,-16.23 -10.57,-28.84 -11.98,-31.18 l -75.82,-75.82 c -6.95,3.64 -14.5,8.63 -22.52,14.89 l 58.13,58.14 -0.03,0.03 c 4.8,5.01 13.98,15.11 20.13,25.07 3.75,6.08 5.87,11.21 6.46,15.66 1.42,7.98 1.06,14.77 -1.09,20.77 -2.09,5.83 -5.51,9.68 -7.91,12.08 -6.83,6.83 -13.16,11.26 -19.37,13.55 -6.33,2.34 -11.55,2.09 -15.39,1.6 -5.05,-0.63 -10.62,-2.28 -16.58,-4.9 L 385.9,353.6 c 35.17,19.04 63.04,15.46 89.79,-11.29 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path43"
d="m 311.53,35.11 c 5.83,2.09 9.68,5.51 12.08,7.91 6.83,6.83 11.26,13.16 13.55,19.37 2.34,6.33 2.09,11.55 1.6,15.39 -0.63,5.05 -2.28,10.62 -4.9,16.58 L 353.6,114.1 C 372.64,78.93 369.06,51.06 342.31,24.31 326.99,8.99 307.36,3.63 283.97,8.38 c -16.23,3.3 -28.84,10.57 -31.18,11.98 l -75.82,75.82 c 3.64,6.95 8.63,14.5 14.89,22.52 L 250,60.57 l 0.03,0.03 c 5.01,-4.8 15.11,-13.98 25.07,-20.13 6.08,-3.75 11.21,-5.87 15.66,-6.46 7.98,-1.41 14.77,-1.05 20.77,1.1 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path45"
d="m 157.69,24.31 c -28.01,28.01 -30.61,57.26 -8.43,94.84 5.34,9.04 12.24,18.64 20.51,28.54 18.86,22.58 45.85,47.88 80.23,75.22 22.52,-17.91 41.95,-35 57.79,-50.83 7.26,-7.26 13.91,-14.4 19.81,-21.27 l -18.76,-18.76 c -5.78,6.83 -12.41,13.98 -19.75,21.32 -10.39,10.39 -22.57,21.49 -36.19,32.99 l -2.9,2.44 -2.89,-2.44 C 222.2,165.33 202.31,145.77 188.02,128.22 178.94,117.08 172.04,106.66 167.49,97.24 163.57,89.1 161.38,81.69 161,75.21 c -0.19,-3.16 -0.15,-8.05 2.3,-14 2.4,-5.84 6.68,-11.79 13.09,-18.2 2.39,-2.39 6.25,-5.82 12.08,-7.91 6.02,-2.16 12.85,-2.51 20.87,-1.07 l 0.11,0.02 c 3.49,0.72 7.45,2.35 12.03,4.95 L 242.64,17.84 C 237.13,15.01 227.48,10.7 216.02,8.37 192.64,3.63 173.01,8.99 157.69,24.31 Z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path47"
d="m 276.85,50 c -8.23,5.56 -16.22,12.85 -20.49,16.92 l 93.37,93.37 c 7.42,-6.02 14.68,-11.29 21.67,-15.74 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path49"
d="m 24.31,342.31 c 28.01,28.01 57.26,30.61 94.84,8.43 9.04,-5.34 18.64,-12.24 28.54,-20.51 22.58,-18.86 47.88,-45.85 75.22,-80.23 -17.91,-22.52 -35,-41.95 -50.83,-57.79 -7.26,-7.26 -14.4,-13.91 -21.27,-19.81 l -18.76,18.76 c 6.83,5.78 13.98,12.41 21.32,19.75 10.39,10.39 21.5,22.57 32.99,36.19 l 2.44,2.89 -2.44,2.89 c -21.03,24.91 -40.59,44.8 -58.14,59.09 -11.14,9.08 -21.56,15.98 -30.98,20.53 -8.14,3.92 -15.55,6.11 -22.03,6.49 -3.16,0.19 -8.05,0.15 -14,-2.3 -5.84,-2.4 -11.79,-6.68 -18.2,-13.09 -2.39,-2.39 -5.82,-6.25 -7.91,-12.08 -2.16,-6.02 -2.51,-12.85 -1.07,-20.88 l 0.02,-0.11 C 34.77,287.04 36.4,283.08 39,278.5 L 17.84,257.34 c -2.83,5.51 -7.14,15.17 -9.47,26.63 -4.74,23.39 0.62,43.02 15.94,58.34 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path51"
d="m 60.57,250 0.03,-0.03 c -4.8,-5.02 -13.98,-15.11 -20.13,-25.07 -3.75,-6.08 -5.87,-11.21 -6.46,-15.66 -1.42,-7.98 -1.06,-14.77 1.09,-20.77 2.09,-5.83 5.51,-9.68 7.91,-12.08 6.83,-6.83 13.16,-11.26 19.37,-13.55 6.33,-2.34 11.55,-2.09 15.39,-1.6 5.05,0.63 10.62,2.28 16.58,4.9 L 114.09,146.4 C 78.92,127.36 51.05,130.94 24.3,157.69 8.99,173.01 3.63,192.64 8.38,216.03 c 3.3,16.23 10.57,28.84 11.98,31.18 l 75.82,75.82 c 6.95,-3.64 14.5,-8.63 22.52,-14.89 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path53"
d="m 66.93,243.64 93.37,-93.37 c -6.02,-7.42 -11.29,-14.68 -15.74,-21.67 L 50,223.15 c 5.57,8.23 12.85,16.23 16.93,20.49 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path55"
d="m 342.31,475.69 c 28.01,-28.01 30.61,-57.26 8.43,-94.84 -5.34,-9.04 -12.24,-18.64 -20.51,-28.54 -18.86,-22.58 -45.85,-47.88 -80.23,-75.22 -22.52,17.91 -41.95,35 -57.79,50.83 -7.26,7.26 -13.91,14.4 -19.81,21.27 l 18.76,18.76 c 5.78,-6.83 12.41,-13.98 19.75,-21.32 10.39,-10.39 22.57,-21.49 36.19,-32.99 l 2.89,-2.44 2.89,2.44 c 24.91,21.03 44.8,40.59 59.09,58.14 9.08,11.14 15.98,21.56 20.53,30.98 3.93,8.14 6.11,15.56 6.49,22.03 0.19,3.16 0.15,8.05 -2.3,14 -2.4,5.84 -6.68,11.79 -13.09,18.2 -2.39,2.39 -6.25,5.82 -12.08,7.91 -6.02,2.16 -12.85,2.51 -20.88,1.07 l -0.11,-0.02 c -3.49,-0.72 -7.45,-2.35 -12.03,-4.95 l -21.16,21.16 c 5.5,2.83 15.15,7.13 26.59,9.46 23.41,4.76 43.05,-0.6 58.38,-15.93 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path57"
d="M 243.64,433.07 150.27,339.7 c -7.42,6.02 -14.68,11.29 -21.67,15.74 L 223.15,450 c 8.23,-5.57 16.23,-12.85 20.49,-16.93 z"
class="st0" />
<path
style="fill:#000000;stroke:#000000;stroke-opacity:1;stroke-width:7.4999999;stroke-miterlimit:4;stroke-dasharray:none"
inkscape:connector-curvature="0"
id="path59"
d="m 157.69,475.69 c 15.32,15.32 34.95,20.68 58.33,15.93 16.24,-3.3 28.85,-10.58 31.19,-11.98 l 75.82,-75.82 c -3.64,-6.95 -8.63,-14.5 -14.89,-22.52 L 250,439.43 249.97,439.4 c -5.01,4.8 -15.11,13.98 -25.07,20.13 -6.08,3.75 -11.21,5.87 -15.66,6.46 -7.98,1.42 -14.77,1.06 -20.77,-1.09 -5.83,-2.09 -9.68,-5.51 -12.08,-7.91 -6.83,-6.83 -11.26,-13.16 -13.55,-19.37 -2.34,-6.33 -2.09,-11.55 -1.6,-15.39 0.63,-5.05 2.28,-10.62 4.9,-16.58 L 146.4,385.9 c -19.04,35.17 -15.46,63.04 11.29,89.79 z"
class="st0" />
</g><g
id="g28"
transform="translate(2.1186441)">
<path
class="st0"
d="m 465.94,209.46 c -0.72,3.49 -2.35,7.45 -4.95,12.03 l 21.16,21.16 c 2.83,-5.51 7.14,-15.16 9.47,-26.61 4.76,-23.39 -0.6,-43.02 -15.93,-58.35 -28.01,-28.01 -57.26,-30.61 -94.84,-8.43 -9.04,5.34 -18.64,12.24 -28.55,20.51 -22.58,18.86 -47.88,45.85 -75.22,80.23 17.91,22.52 35,41.95 50.83,57.79 7.26,7.26 14.4,13.91 21.27,19.81 l 18.76,-18.76 c -6.83,-5.78 -13.98,-12.41 -21.32,-19.75 -10.39,-10.39 -21.5,-22.57 -32.99,-36.19 l -2.43,-2.9 2.44,-2.89 c 21.03,-24.91 40.59,-44.8 58.14,-59.09 11.14,-9.08 21.56,-15.98 30.98,-20.53 8.14,-3.92 15.55,-6.11 22.03,-6.49 3.16,-0.19 8.05,-0.15 14,2.3 5.84,2.4 11.79,6.68 18.2,13.09 2.39,2.39 5.82,6.25 7.91,12.08 2.16,6.02 2.51,12.85 1.07,20.88 z"
id="path4"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="M 355.44,371.4 450,276.85 c -5.56,-8.23 -12.85,-16.22 -16.93,-20.49 l -93.37,93.37 c 6.02,7.42 11.29,14.68 15.74,21.67 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 475.69,342.31 c 15.32,-15.32 20.68,-34.95 15.93,-58.34 -3.3,-16.23 -10.57,-28.84 -11.98,-31.18 l -75.82,-75.82 c -6.95,3.64 -14.5,8.63 -22.52,14.89 l 58.13,58.14 -0.03,0.03 c 4.8,5.01 13.98,15.11 20.13,25.07 3.75,6.08 5.87,11.21 6.46,15.66 1.42,7.98 1.06,14.77 -1.09,20.77 -2.09,5.83 -5.51,9.68 -7.91,12.08 -6.83,6.83 -13.16,11.26 -19.37,13.55 -6.33,2.34 -11.55,2.09 -15.39,1.6 -5.05,-0.63 -10.62,-2.28 -16.58,-4.9 L 385.9,353.6 c 35.17,19.04 63.04,15.46 89.79,-11.29 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 311.53,35.11 c 5.83,2.09 9.68,5.51 12.08,7.91 6.83,6.83 11.26,13.16 13.55,19.37 2.34,6.33 2.09,11.55 1.6,15.39 -0.63,5.05 -2.28,10.62 -4.9,16.58 L 353.6,114.1 C 372.64,78.93 369.06,51.06 342.31,24.31 326.99,8.99 307.36,3.63 283.97,8.38 c -16.23,3.3 -28.84,10.57 -31.18,11.98 l -75.82,75.82 c 3.64,6.95 8.63,14.5 14.89,22.52 L 250,60.57 l 0.03,0.03 c 5.01,-4.8 15.11,-13.98 25.07,-20.13 6.08,-3.75 11.21,-5.87 15.66,-6.46 7.98,-1.41 14.77,-1.05 20.77,1.1 z"
id="path10"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 157.69,24.31 c -28.01,28.01 -30.61,57.26 -8.43,94.84 5.34,9.04 12.24,18.64 20.51,28.54 18.86,22.58 45.85,47.88 80.23,75.22 22.52,-17.91 41.95,-35 57.79,-50.83 7.26,-7.26 13.91,-14.4 19.81,-21.27 l -18.76,-18.76 c -5.78,6.83 -12.41,13.98 -19.75,21.32 -10.39,10.39 -22.57,21.49 -36.19,32.99 l -2.9,2.44 -2.89,-2.44 C 222.2,165.33 202.31,145.77 188.02,128.22 178.94,117.08 172.04,106.66 167.49,97.24 163.57,89.1 161.38,81.69 161,75.21 c -0.19,-3.16 -0.15,-8.05 2.3,-14 2.4,-5.84 6.68,-11.79 13.09,-18.2 2.39,-2.39 6.25,-5.82 12.08,-7.91 6.02,-2.16 12.85,-2.51 20.87,-1.07 l 0.11,0.02 c 3.49,0.72 7.45,2.35 12.03,4.95 L 242.64,17.84 C 237.13,15.01 227.48,10.7 216.02,8.37 192.64,3.63 173.01,8.99 157.69,24.31 Z"
id="path12"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 276.85,50 c -8.23,5.56 -16.22,12.85 -20.49,16.92 l 93.37,93.37 c 7.42,-6.02 14.68,-11.29 21.67,-15.74 z"
id="path14"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 24.31,342.31 c 28.01,28.01 57.26,30.61 94.84,8.43 9.04,-5.34 18.64,-12.24 28.54,-20.51 22.58,-18.86 47.88,-45.85 75.22,-80.23 -17.91,-22.52 -35,-41.95 -50.83,-57.79 -7.26,-7.26 -14.4,-13.91 -21.27,-19.81 l -18.76,18.76 c 6.83,5.78 13.98,12.41 21.32,19.75 10.39,10.39 21.5,22.57 32.99,36.19 l 2.44,2.89 -2.44,2.89 c -21.03,24.91 -40.59,44.8 -58.14,59.09 -11.14,9.08 -21.56,15.98 -30.98,20.53 -8.14,3.92 -15.55,6.11 -22.03,6.49 -3.16,0.19 -8.05,0.15 -14,-2.3 -5.84,-2.4 -11.79,-6.68 -18.2,-13.09 -2.39,-2.39 -5.82,-6.25 -7.91,-12.08 -2.16,-6.02 -2.51,-12.85 -1.07,-20.88 l 0.02,-0.11 C 34.77,287.04 36.4,283.08 39,278.5 L 17.84,257.34 c -2.83,5.51 -7.14,15.17 -9.47,26.63 -4.74,23.39 0.62,43.02 15.94,58.34 z"
id="path16"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 60.57,250 0.03,-0.03 c -4.8,-5.02 -13.98,-15.11 -20.13,-25.07 -3.75,-6.08 -5.87,-11.21 -6.46,-15.66 -1.42,-7.98 -1.06,-14.77 1.09,-20.77 2.09,-5.83 5.51,-9.68 7.91,-12.08 6.83,-6.83 13.16,-11.26 19.37,-13.55 6.33,-2.34 11.55,-2.09 15.39,-1.6 5.05,0.63 10.62,2.28 16.58,4.9 L 114.09,146.4 C 78.92,127.36 51.05,130.94 24.3,157.69 8.99,173.01 3.63,192.64 8.38,216.03 c 3.3,16.23 10.57,28.84 11.98,31.18 l 75.82,75.82 c 6.95,-3.64 14.5,-8.63 22.52,-14.89 z"
id="path18"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 66.93,243.64 93.37,-93.37 c -6.02,-7.42 -11.29,-14.68 -15.74,-21.67 L 50,223.15 c 5.57,8.23 12.85,16.23 16.93,20.49 z"
id="path20"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 342.31,475.69 c 28.01,-28.01 30.61,-57.26 8.43,-94.84 -5.34,-9.04 -12.24,-18.64 -20.51,-28.54 -18.86,-22.58 -45.85,-47.88 -80.23,-75.22 -22.52,17.91 -41.95,35 -57.79,50.83 -7.26,7.26 -13.91,14.4 -19.81,21.27 l 18.76,18.76 c 5.78,-6.83 12.41,-13.98 19.75,-21.32 10.39,-10.39 22.57,-21.49 36.19,-32.99 l 2.89,-2.44 2.89,2.44 c 24.91,21.03 44.8,40.59 59.09,58.14 9.08,11.14 15.98,21.56 20.53,30.98 3.93,8.14 6.11,15.56 6.49,22.03 0.19,3.16 0.15,8.05 -2.3,14 -2.4,5.84 -6.68,11.79 -13.09,18.2 -2.39,2.39 -6.25,5.82 -12.08,7.91 -6.02,2.16 -12.85,2.51 -20.88,1.07 l -0.11,-0.02 c -3.49,-0.72 -7.45,-2.35 -12.03,-4.95 l -21.16,21.16 c 5.5,2.83 15.15,7.13 26.59,9.46 23.41,4.76 43.05,-0.6 58.38,-15.93 z"
id="path22"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="M 243.64,433.07 150.27,339.7 c -7.42,6.02 -14.68,11.29 -21.67,15.74 L 223.15,450 c 8.23,-5.57 16.23,-12.85 20.49,-16.93 z"
id="path24"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
<path
class="st0"
d="m 157.69,475.69 c 15.32,15.32 34.95,20.68 58.33,15.93 16.24,-3.3 28.85,-10.58 31.19,-11.98 l 75.82,-75.82 c -3.64,-6.95 -8.63,-14.5 -14.89,-22.52 L 250,439.43 249.97,439.4 c -5.01,4.8 -15.11,13.98 -25.07,20.13 -6.08,3.75 -11.21,5.87 -15.66,6.46 -7.98,1.42 -14.77,1.06 -20.77,-1.09 -5.83,-2.09 -9.68,-5.51 -12.08,-7.91 -6.83,-6.83 -11.26,-13.16 -13.55,-19.37 -2.34,-6.33 -2.09,-11.55 -1.6,-15.39 0.63,-5.05 2.28,-10.62 4.9,-16.58 L 146.4,385.9 c -19.04,35.17 -15.46,63.04 11.29,89.79 z"
id="path26"
inkscape:connector-curvature="0"
style="fill:#ffffff" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -17,10 +17,10 @@
style="enable-background:new 0 0 24 24;" style="enable-background:new 0 0 24 24;"
xml:space="preserve" xml:space="preserve"
sodipodi:docname="negative_heart_24px.svg" sodipodi:docname="negative_heart_24px.svg"
inkscape:export-filename="/home/sarah/PARA/projects/cwtch/assets/core/negative_heart_256px.png" inkscape:export-filename="/home/sarah/AndroidStudioProjects/flutter_app/assets/core/negative_heart_512px.png"
inkscape:export-xdpi="1024" inkscape:export-xdpi="4096"
inkscape:export-ydpi="1024" inkscape:export-ydpi="4096"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"><metadata
id="metadata14"><rdf:RDF><cc:Work id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
@ -34,14 +34,14 @@
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1020" inkscape:window-height="1015"
id="namedview10" id="namedview10"
showgrid="false" showgrid="false"
inkscape:zoom="9.8333333" inkscape:zoom="9.8333333"
inkscape:cx="-5.8983051" inkscape:cx="12"
inkscape:cy="14.281162" inkscape:cy="14.687942"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="31" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /> inkscape:current-layer="Layer_1" />
<style <style

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

BIN
cwtch.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,6 +1,6 @@
Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.0.18/tor-win64-0.4.5.9.zip -OutFile tor.zip Invoke-WebRequest -Uri https://www.torproject.org/dist/torbrowser/10.0.16/tor-win32-0.4.5.7.zip -OutFile tor.zip
if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '72764eb07ad8ab511603aba0734951ca003989f5f4686af91ba220217b4a8a4bcc5f571b59f52c847932f6efedf847b111621983050fcddbb8099d43ca66fb07' ) { Write-Error 'tor.zip sha512sum mismatch' } if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '2b7d683f036d0fec149f1d2bdfcf5b7ef4c337005a2b685c056b00047fdb2b57d4c25b8559ad7ef5c7a030b273934be82a9f83ef6e391f5d7d13d8d6c83e8048' ) { Write-Error 'tor.zip sha512sum mismatch' }
Expand-Archive -Path tor.zip -DestinationPath Tor Expand-Archive -Path tor.zip -DestinationPath Tor

View File

@ -1,12 +0,0 @@
#!/bin/sh
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.5.9-linux-x86_64 -O linux/tor
chmod a+x linux/tor
mkdir -p android/app/src/main/jniLibs/arm64-v8a
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm64_pie -O android/app/src/main/jniLibs/arm64-v8a/libtor.so
chmod a+x android/app/src/main/jniLibs/arm64-v8a/libtor.so
mkdir -p android/app/src/main/jniLibs/armeabi-v7a
wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.4.9-arm_pie -O android/app/src/main/jniLibs/armeabi-v7a/libtor.so
chmod a+x android/app/src/main/jniLibs/armeabi-v7a/libtor.so

View File

@ -1,6 +0,0 @@
const dev_version = "development";
class EnvironmentConfig {
static const BUILD_VER = String.fromEnvironment('BUILD_VER', defaultValue: dev_version);
static const BUILD_DATE = String.fromEnvironment('BUILD_DATE', defaultValue: "now");
}

View File

@ -1,10 +1,6 @@
import 'package:flutter/src/services/text_input.dart';
abstract class Cwtch { abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<void> Start(); Future<void> Start();
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground();
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SelectProfile(String onion); void SelectProfile(String onion);
@ -12,8 +8,6 @@ abstract class Cwtch {
void CreateProfile(String nick, String pass); void CreateProfile(String nick, String pass);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void LoadProfiles(String pass); void LoadProfiles(String pass);
// ignore: non_constant_identifier_names
void DeleteProfile(String onion, String pass);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ResetTor(); void ResetTor();
@ -28,24 +22,28 @@ abstract class Cwtch {
void AcceptContact(String profileOnion, String contactHandle); void AcceptContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void BlockContact(String profileOnion, String contactHandle); void BlockContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index); Future<String> ACNEvents();
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void UpdateMessageFlags(String profile, String handle, int index, int flags); Future<String> ContactEvents();
// ignore: non_constant_identifier_names
Future<String> GetProfiles();
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle);
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index);
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profile, String handle, String message); void SendMessage(String profile, String handle, String message);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profile, String handle, String target); void SendInvitation(String profile, String handle, String target);
// ignore: non_constant_identifier_names
void LeaveConversation(String profile, String handle);
// ignore: non_constant_identifier_names
void CreateGroup(String profile, String server, String groupName);
// ignore: non_constant_identifier_names
void LeaveGroup(String profile, String groupID);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ImportBundle(String profile, String bundle); void ImportBundle(String profile, String bundle);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
@ -53,8 +51,5 @@ abstract class Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle); void RejectInvite(String profileOnion, String groupHandle);
// ignore: non_constant_identifier_names
void Shutdown();
void dispose(); void dispose();
} }

View File

@ -1,6 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/models/servers.dart';
import 'package:cwtch/notification_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:cwtch/torstatus.dart'; import 'package:cwtch/torstatus.dart';
@ -12,36 +10,26 @@ import '../settings.dart';
// Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin) // Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
// Takes Notifiers and triggers them on appropriate events // Takes Notifiers and triggers them on appropriate events
class CwtchNotifier { class CwtchNotifier {
late ProfileListState profileCN; ProfileListState profileCN;
late Settings settings; Settings settings;
late ErrorHandler error; ErrorHandler error;
late TorStatus torStatus; TorStatus torStatus;
late NotificationsManager notificationManager;
late AppState appState;
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN) { CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN) {
profileCN = pcn; profileCN = pcn;
settings = settingsCN; settings = settingsCN;
error = errorCN; error = errorCN;
torStatus = torStatusCN; torStatus = torStatusCN;
notificationManager = notificationManagerP;
appState = appStateCN;
} }
void handleMessage(String type, dynamic data) { void handleMessage(String type, dynamic data) {
switch (type) { switch (type) {
case "CwtchStarted":
appState.SetCwtchInit();
break;
case "CwtchStartError":
appState.SetAppError(data["Error"]);
break;
case "NewPeer": case "NewPeer":
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta... profileCN.add(ProfileInfoState(
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword"); onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], serversJson: data["ServerList"], online: data["Online"] == "true"));
break; break;
case "PeerCreated": case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState( profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(
data["ProfileOnion"], data["ProfileOnion"],
data["RemotePeer"], data["RemotePeer"],
nickname: data["nick"], nickname: data["nick"],
@ -49,41 +37,16 @@ class CwtchNotifier {
imagePath: data["picture"], imagePath: data["picture"],
isBlocked: data["authorization"] == "blocked", isBlocked: data["authorization"] == "blocked",
isInvitation: data["authorization"] == "unknown", isInvitation: data["authorization"] == "unknown",
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"], savePeerHistory: data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]), numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]), numUnread: int.parse(data["unread"]),
isGroup: data["isGroup"] == true, isGroup: data["isGroup"],
server: data["groupServer"], server: data["groupServer"],
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
)); ));
break; break;
case "GroupCreated":
// Retrieve Server Status from Cache...
String status = "";
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
if (serverInfoState != null) {
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"],
isInvitation: false, imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
}
break;
case "PeerDeleted":
profileCN.delete(data["Identity"]);
// todo standarize
error.handleUpdate("deleteprofile.success");
break;
case "DeleteContact":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]);
break;
case "DeleteGroup":
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["GroupID"]);
break;
case "PeerStateChange": case "PeerStateChange":
ContactInfoState? contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]); ContactInfoState contact = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]);
if (contact != null) { if (contact != null) {
if (data["ConnectionState"] != null) { if (data["ConnectionState"] != null) {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
@ -93,16 +56,13 @@ class CwtchNotifier {
contact.isBlocked = data["authorization"] == "blocked"; contact.isBlocked = data["authorization"] == "blocked";
} }
// contact.[status/isBlocked] might change the list's sort order // contact.[status/isBlocked] might change the list's sort order
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort(); profileCN.getProfile(data["ProfileOnion"]).contactList.resort();
} }
break; break;
case "NewMessageFromPeer": case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!"); profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).unreadMessages++;
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) { profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break; break;
case "PeerAcknowledgement": case "PeerAcknowledgement":
// We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end... // We don't use these anymore, IndexedAcknowledgement is more suited to the UI front end...
@ -111,10 +71,10 @@ class CwtchNotifier {
var idx = data["Index"]; var idx = data["Index"];
// We return -1 for protocol message acks if there is no message // We return -1 for protocol message acks if there is no message
if (idx == "-1") break; if (idx == "-1") break;
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["RemotePeer"]).getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageState>(key.currentContext, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -125,18 +85,16 @@ class CwtchNotifier {
case "NewMessageFromGroup": case "NewMessageFromGroup":
if (data["ProfileOnion"] != data["RemotePeer"]) { if (data["ProfileOnion"] != data["RemotePeer"]) {
//not from me //not from me
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) { profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).unreadMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++; profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).totalMessages++;
} profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
} else { } else {
// from me (already displayed - do not update counter) // from me (already displayed - do not update counter)
var idx = data["Signature"]; var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageState>(key.currentContext, listen: false);
if (message == null) break; if (message == null) break;
message.ackd = true; message.ackd = true;
} catch (e) { } catch (e) {
@ -144,30 +102,14 @@ class CwtchNotifier {
} }
} }
break; break;
case "MessageCounterResync":
var contactHandle = data["RemotePeer"];
if (contactHandle == null || contactHandle == "") contactHandle = data["GroupID"];
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = int.parse(data["Data"]);
break;
case "IndexedFailure":
print("IndexedFailure: $data");
var idx = data["Index"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.getMessageKey(idx);
try {
var message = Provider.of<MessageState>(key!.currentContext!, listen: false);
message.error = true;
} catch (e) {
// ignore, we likely have an old key that has been replaced with an actual signature
}
break;
case "SendMessageToGroupError": case "SendMessageToGroupError":
// from me (already displayed - do not update counter) // from me (already displayed - do not update counter)
print("SendMessageToGroupError: $data"); print("SendMessageToGroupError: $data");
var idx = data["Signature"]; var idx = data["Signature"];
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.getMessageKey(idx); var key = profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).getMessageKey(idx);
if (key == null) break; if (key == null) break;
try { try {
var message = Provider.of<MessageState>(key.currentContext!, listen: false); var message = Provider.of<MessageState>(key.currentContext, listen: false);
if (message == null) break; if (message == null) break;
message.error = true; message.error = true;
} catch (e) { } catch (e) {
@ -176,105 +118,60 @@ class CwtchNotifier {
break; break;
case "AppError": case "AppError":
print("New App Error: $data"); print("New App Error: $data");
// special case for delete error (todo: standardize cwtch errors) error.handleUpdate(data["Data"]);
if (data["Error"] == "Password did not match") {
error.handleUpdate("deleteprofile.error");
} else if (data["Data"] != null) {
error.handleUpdate(data["Data"]);
}
break; break;
case "UpdateGlobalSettings": case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"])); settings.handleUpdate(jsonDecode(data["Data"]));
break; break;
case "SetAttribute": case "SetAttribute":
if (data["Key"] == "public.name") { if (data["Key"] == "public.name") {
profileCN.getProfile(data["ProfileOnion"])?.nickname = data["Data"]; profileCN.getProfile(data["ProfileOnion"]).nickname = data["Data"];
} else { } else {
print("unhandled set attribute event: $type $data"); print("unhandled set attribute event: $type");
} }
break; break;
case "NetworkError": case "NetworkError":
var isOnline = data["Status"] == "Success"; var isOnline = data["Status"] == "Success";
profileCN.getProfile(data["ProfileOnion"])?.isOnline = isOnline; profileCN.getProfile(data["ProfileOnion"]).isOnline = isOnline;
break; break;
case "ACNStatus": case "ACNStatus":
print("acn status: $data"); print("acn status: $data");
torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]); torStatus.handleUpdate(int.parse(data["Progress"]), data["Status"]);
break; break;
case "ACNVersion":
print("acn version: $data");
torStatus.updateVersion(data["Data"]);
break;
case "UpdateServerInfo": case "UpdateServerInfo":
profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); profileCN.getProfile(data["ProfileOnion"]).replaceServers(data["ServerList"]);
break; break;
case "NewGroup": case "NewGroup":
print("new group: $data"); print("new group invite: $data");
String invite = data["GroupInvite"].toString(); String invite = data["GroupInvite"].toString();
if (invite.startsWith("torv3")) { if (invite.startsWith("torv3")) {
String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5))); String inviteJson = new String.fromCharCodes(base64Decode(invite.substring(5)));
dynamic groupInvite = jsonDecode(inviteJson); dynamic groupInvite = jsonDecode(inviteJson);
print("group invite: $groupInvite"); print("new group invite: $groupInvite");
if (profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(groupInvite["GroupID"]) == null) {
// Retrieve Server Status from Cache... profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
String status = ""; isInvitation: true, imagePath: data["PicturePath"], nickname: groupInvite["GroupName"], server: groupInvite["ServerHost"], isGroup: true, lastMessageTime: DateTime.now()));
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]); profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.now());
if (serverInfoState != null) {
print("Got server status: " + serverInfoState.status);
status = serverInfoState.status;
}
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
isInvitation: false,
imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"],
status: status,
isGroup: true,
lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(groupInvite["GroupID"], DateTime.now());
} }
} }
break; break;
case "AcceptGroupInvite": case "AcceptGroupInvite":
print("accept group invite: $data"); print("accept group invite: $data");
profileCN.getProfile(data["ProfileOnion"]).contactList.getContact(data["GroupID"]).isInvitation = false;
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isInvitation = false; profileCN.getProfile(data["ProfileOnion"]).contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
break; break;
case "ServerStateChange": case "ServerStateChange":
print("server state change: $data"); print("server state change: $data");
// Update the Server Cache profileCN.getProfile(data["ProfileOnion"]).contactList.contacts.forEach((contact) {
profileCN.getProfile(data["ProfileOnion"])?.updateServerStatusCache(data["GroupServer"], data["ConnectionState"]);
profileCN.getProfile(data["ProfileOnion"])?.contactList.contacts.forEach((contact) {
if (contact.isGroup == true && contact.server == data["GroupServer"]) { if (contact.isGroup == true && contact.server == data["GroupServer"]) {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
} }
}); });
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort(); profileCN.getProfile(data["ProfileOnion"]).contactList.resort();
break;
case "SetGroupAttribute":
if (data["Key"] == "local.name") {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.nickname = data["Data"];
}
} else {
print("unhandled set group attribute event: $type $data");
}
break;
case "NewRetValMessageFromPeer":
if (data["Path"] == "name") {
// Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"];
}
} else {
print("unhandled peer attribute event: $type $data");
}
break; break;
default: default:
print("unhandled event: $type $data"); print("unhandled event: $type");
} }
} }
} }

View File

@ -4,23 +4,17 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:cwtch/cwtch/cwtchNotifier.dart'; import 'package:cwtch/cwtch/cwtchNotifier.dart';
import 'package:flutter/src/services/text_input.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:cwtch/cwtch/cwtch.dart'; import 'package:cwtch/cwtch/cwtch.dart';
import '../config.dart';
///////////////////// /////////////////////
/// Cwtch API /// /// Cwtch API ///
///////////////////// /////////////////////
typedef start_cwtch_function = Int8 Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2); typedef start_cwtch_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = int Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen); typedef StartCwtchFn = void Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef void_from_void_funtion = Void Function();
typedef VoidFromVoidFunction = void Function();
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int); typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
@ -31,9 +25,6 @@ typedef VoidFromStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer
typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32); typedef void_from_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int); typedef VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_int_int_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int64, Int64);
typedef VoidFromStringStringIntIntFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef access_cwtch_eventbus_function = Void Function(); typedef access_cwtch_eventbus_function = Void Function();
typedef NextEventFn = void Function(); typedef NextEventFn = void Function();
@ -61,14 +52,13 @@ typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32); typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int); typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef appbus_events_function = Pointer<Utf8> Function(); typedef acn_events_function = Pointer<Utf8> Function();
typedef AppbusEventsFn = Pointer<Utf8> Function(); typedef ACNEventsFn = Pointer<Utf8> Function();
class CwtchFfi implements Cwtch { class CwtchFfi implements Cwtch {
late DynamicLibrary library; DynamicLibrary library;
late CwtchNotifier cwtchNotifier; CwtchNotifier cwtchNotifier;
late Isolate cwtchIsolate; Isolate cwtchIsolate;
ReceivePort _receivePort = ReceivePort();
CwtchFfi(CwtchNotifier _cwtchNotifier) { CwtchFfi(CwtchNotifier _cwtchNotifier) {
if (Platform.isWindows) { if (Platform.isWindows) {
@ -89,16 +79,12 @@ class CwtchFfi implements Cwtch {
String bundledTor = ""; String bundledTor = "";
Map<String, String> envVars = Platform.environment; Map<String, String> envVars = Platform.environment;
if (Platform.isLinux) { if (Platform.isLinux) {
home = (envVars['HOME'])!; home = envVars['HOME'];
bundledTor = "./tor";
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
home = (envVars['UserProfile'])!; home = envVars['UserProfile'];
bundledTor = "Tor\\Tor\\tor.exe"; bundledTor = "Tor\\Tor\\tor.exe";
} }
var cwtchDir = path.join(home, ".cwtch"); var cwtchDir = path.join(home, ".cwtch/dev/");
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
}
print("cwtchDir $cwtchDir"); print("cwtchDir $cwtchDir");
var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch"); var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
@ -109,6 +95,7 @@ class CwtchFfi implements Cwtch {
StartCwtch(ut8CwtchDir, ut8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length); StartCwtch(ut8CwtchDir, ut8CwtchDir.length, bundledTor.toNativeUtf8(), bundledTor.length);
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier // Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
var _receivePort = ReceivePort();
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort); cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
_receivePort.listen((message) { _receivePort.listen((message) {
var env = jsonDecode(message); var env = jsonDecode(message);
@ -116,14 +103,6 @@ class CwtchFfi implements Cwtch {
}); });
} }
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
var reconnectCwtch = library.lookup<NativeFunction<Void Function()>>("c_ReconnectCwtchForeground");
// ignore: non_constant_identifier_names
final ReconnectCwtchForeground = reconnectCwtch.asFunction<void Function()>();
ReconnectCwtchForeground();
}
// Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
@override @override
void dispose() { void dispose() {
@ -138,30 +117,24 @@ class CwtchFfi implements Cwtch {
await for (var value in stream) { await for (var value in stream) {
sendPort.send(value); sendPort.send(value);
} }
print("checkAppBusEvents finished...");
} }
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it // Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* { static Stream<String> pollAppbusEvents() async* {
late DynamicLibrary library; DynamicLibrary library;
if (Platform.isWindows) { if (Platform.isWindows) {
library = DynamicLibrary.open("libCwtch.dll"); library = DynamicLibrary.open("libCwtch.dll");
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
library = DynamicLibrary.open("libCwtch.so"); library = DynamicLibrary.open("libCwtch.so");
} }
var getAppbusEventC = library.lookup<NativeFunction<appbus_events_function>>("c_GetAppBusEvent"); var getAppbusEventC = library.lookup<NativeFunction<acn_events_function>>("c_GetAppBusEvent");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final GetAppbusEvent = getAppbusEventC.asFunction<AppbusEventsFn>(); final GetAppbusEvent = getAppbusEventC.asFunction<ACNEventsFn>();
while (true) { while (true) {
Pointer<Utf8> result = GetAppbusEvent(); Pointer<Utf8> result = GetAppbusEvent();
String event = result.toDartString(); String event = result.toDartString();
if (event.startsWith("{\"EventType\":\"Shutdown\"")) {
print("Shutting down isolate thread: $event");
return;
}
yield event; yield event;
} }
} }
@ -194,6 +167,50 @@ class CwtchFfi implements Cwtch {
LoadProfiles(ut8pass, ut8pass.length); LoadProfiles(ut8pass, ut8pass.length);
} }
// ignore: non_constant_identifier_names
Future<String> ACNEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>("c_ACNEvents");
// ignore: non_constant_identifier_names
final ACNEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ACNEvents();
String event = result.toDartString();
return event;
}
// ignore: non_constant_identifier_names
Future<String> ContactEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>("c_ContactEvents");
// ignore: non_constant_identifier_names
final ContactEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ContactEvents();
String event = result.toDartString();
return event;
}
// ignore: non_constant_identifier_names
Future<String> GetProfiles() async {
var getProfilesC = library.lookup<NativeFunction<get_json_blob_void_function>>("c_GetProfiles");
// ignore: non_constant_identifier_names
final GetProfiles = getProfilesC.asFunction<GetJsonBlobVoidFn>();
Pointer<Utf8> jsonProfilesBytes = GetProfiles();
String jsonProfiles = jsonProfilesBytes.toDartString();
return jsonProfiles;
}
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle) async {
var numMessagesC = library.lookup<NativeFunction<get_int_from_str_str_function>>("c_NumMessages");
// ignore: non_constant_identifier_names
final NumMessages = numMessagesC.asFunction<GetIntFromStrStrFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
int num = NumMessages(utf8profile, utf8profile.length, utf8handle, utf8handle.length);
return num;
}
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) async { Future<String> GetMessage(String profile, String handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage"); var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage");
@ -206,6 +223,18 @@ class CwtchFfi implements Cwtch {
return jsonMessage; return jsonMessage;
} }
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_int_function>>("c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessagesBytes = GetMessages(utf8profile, utf8profile.length, utf8handle, utf8handle.length, start, end);
String jsonMessages = jsonMessagesBytes.toDartString();
return jsonMessages;
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String json) { void SendProfileEvent(String onion, String json) {
@ -249,6 +278,17 @@ class CwtchFfi implements Cwtch {
BlockContact(u1, u1.length, u2, u2.length); BlockContact(u1, u1.length, u2, u2.length);
} }
@override
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle) {
var debugResetContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DebugResetContact");
// ignore: non_constant_identifier_names
final DebugResetContact = debugResetContact.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = contactHandle.toNativeUtf8();
DebugResetContact(u1, u1.length, u2, u2.length);
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, String contactHandle, String message) {
@ -316,75 +356,4 @@ class CwtchFfi implements Cwtch {
final u2 = groupHandle.toNativeUtf8(); final u2 = groupHandle.toNativeUtf8();
RejectInvite(u1, u1.length, u2, u2.length); RejectInvite(u1, u1.length, u2, u2.length);
} }
@override
void CreateGroup(String profileOnion, String server, String groupName) {
var createGroup = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_CreateGroup");
// ignore: non_constant_identifier_names
final CreateGroup = createGroup.asFunction<VoidFromStringStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = server.toNativeUtf8();
final u3 = groupName.toNativeUtf8();
CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length);
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String handle) {
var leaveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveConversation");
// ignore: non_constant_identifier_names
final LeaveConversation = leaveConversation.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = handle.toNativeUtf8();
LeaveConversation(u1, u1.length, u2, u2.length);
}
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
var leaveGroup = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveGroup");
// ignore: non_constant_identifier_names
final LeaveGroup = leaveGroup.asFunction<VoidFromStringStringFn>();
final u1 = profileOnion.toNativeUtf8();
final u2 = groupHandle.toNativeUtf8();
LeaveGroup(u1, u1.length, u2, u2.length);
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
var updateMessageFlagsC = library.lookup<NativeFunction<void_from_string_string_int_int_function>>("c_UpdateMessageFlags");
// ignore: non_constant_identifier_names
final updateMessageFlags = updateMessageFlagsC.asFunction<VoidFromStringStringIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
}
@override
// ignore: non_constant_identifier_names
void DeleteProfile(String onion, String currentPassword) {
var deleteprofile = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteProfile");
// ignore: non_constant_identifier_names
final DeleteProfile = deleteprofile.asFunction<VoidFromStringStringFn>();
final u1 = onion.toNativeUtf8();
final u2 = currentPassword.toNativeUtf8();
DeleteProfile(u1, u1.length, u2, u2.length);
}
@override
Future<void> Shutdown() async {
var shutdown = library.lookup<NativeFunction<void_from_void_funtion>>("c_ShutdownCwtch");
// ignore: non_constant_identifier_names
// Shutdown Cwtch + Tor...
final Shutdown = shutdown.asFunction<VoidFromVoidFunction>();
Shutdown();
// Kill our Isolate
cwtchIsolate.kill(priority: Isolate.immediate);
print("Isolate killed");
_receivePort.close();
print("Receive Port Closed");
}
} }

View File

@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:cwtch/config.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'dart:async'; import 'dart:async';
@ -26,9 +26,9 @@ class CwtchGomobile implements Cwtch {
final appbusEventChannelName = 'test.flutter.dev/eventBus'; final appbusEventChannelName = 'test.flutter.dev/eventBus';
late Future<dynamic> androidLibraryDir; Future<String> androidLibraryDir;
late Future<dynamic> androidHomeDirectory; Future<Directory> androidHomeDirectory;
late CwtchNotifier cwtchNotifier; CwtchNotifier cwtchNotifier;
CwtchGomobile(CwtchNotifier _cwtchNotifier) { CwtchGomobile(CwtchNotifier _cwtchNotifier) {
print("gomobile.dart: CwtchGomobile()"); print("gomobile.dart: CwtchGomobile()");
@ -44,19 +44,10 @@ class CwtchGomobile implements Cwtch {
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<void> Start() async { Future<void> Start() async {
print("gomobile.dart: Start()..."); print("gomobile.dart: Start()...");
var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch"); var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch/dev/");
if (EnvironmentConfig.BUILD_VER == dev_version) {
cwtchDir = path.join(cwtchDir, "dev");
}
String torPath = path.join(await androidLibraryDir, "libtor.so"); String torPath = path.join(await androidLibraryDir, "libtor.so");
print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)..."); print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)...");
return cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath}); cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
}
@override
// ignore: non_constant_identifier_names
Future<void> ReconnectCwtchForeground() async {
cwtchPlatform.invokeMethod("ReconnectCwtchForeground", {});
} }
// Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier // Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier
@ -82,16 +73,37 @@ class CwtchGomobile implements Cwtch {
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DeleteProfile(String onion, String pass) { Future<String> ACNEvents() {
cwtchPlatform.invokeMethod("DeleteProfile", {"ProfileOnion": onion, "pass": pass}); return cwtchPlatform.invokeMethod("ACNEvents");
} }
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
Future<dynamic> GetMessage(String profile, String handle, int index) { Future<String> ContactEvents() {
return cwtchPlatform.invokeMethod("ContactEvents");
}
// ignore: non_constant_identifier_names
Future<String> GetProfiles() {
print("gomobile.dart: GetProfiles()");
return cwtchPlatform.invokeMethod("GetProfiles");
}
// ignore: non_constant_identifier_names
Future<int> NumMessages(String profile, String handle) {
return cwtchPlatform.invokeMethod("NumMessages", {"profile": profile, "contact": handle});
}
// ignore: non_constant_identifier_names
Future<String> GetMessage(String profile, String handle, int index) {
print("gomobile.dart GetMessage " + index.toString()); print("gomobile.dart GetMessage " + index.toString());
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index}); return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "index": index});
} }
// ignore: non_constant_identifier_names
Future<String> GetMessages(String profile, String handle, int start, int end) {
return cwtchPlatform.invokeMethod("GetMessage", {"profile": profile, "contact": handle, "start": start, "end": end});
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String jsonEvent) { void SendProfileEvent(String onion, String jsonEvent) {
@ -119,6 +131,12 @@ class CwtchGomobile implements Cwtch {
cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle}); cwtchPlatform.invokeMethod("BlockContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
} }
@override
// ignore: non_constant_identifier_names
void DebugResetContact(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("DebugResetContact", {"ProfileOnion": profileOnion, "handle": contactHandle});
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, String contactHandle, String message) { void SendMessage(String profileOnion, String contactHandle, String message) {
@ -152,35 +170,6 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void RejectInvite(String profileOnion, String groupHandle) { void RejectInvite(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "groupHandle": groupHandle}); cwtchPlatform.invokeMethod("RejectInvite", {"ProfileOnion": profileOnion, "handle": groupHandle});
}
@override
void CreateGroup(String profileOnion, String server, String groupName) {
cwtchPlatform.invokeMethod("CreateGroup", {"ProfileOnion": profileOnion, "server": server, "groupName": groupName});
}
@override
// ignore: non_constant_identifier_names
void LeaveGroup(String profileOnion, String groupHandle) {
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
}
@override
// ignore: non_constant_identifier_names
void LeaveConversation(String profileOnion, String contactHandle) {
cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": contactHandle});
}
@override
void UpdateMessageFlags(String profile, String handle, int index, int flags) {
print("gomobile.dart UpdateMessageFlags " + index.toString());
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "index": index, "flags": flags});
}
@override
Future<void> Shutdown() async {
print("gomobile.dart Shutdown");
cwtchPlatform.invokeMethod("Shutdown", {});
} }
} }

View File

@ -1,110 +0,0 @@
/// Flutter icons CwtchIcons
/// Copyright (C) 2021 by Open Privacy Research Society via fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
/// following in your pubspec.yaml
///
/// flutter:
/// fonts:
/// - family: CwtchIcons
/// fonts:
/// - asset: assets/fonts/CwtchIcons.ttf
///
///
///
import 'package:flutter/widgets.dart';
class CwtchIcons {
CwtchIcons._();
static const _kFontFam = 'CwtchIcons';
static const String? _kFontPkg = null;
static const IconData arrow_back_24px = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData attach_file_24px = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_peer = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_unknown = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData block_24px = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData brightness_5_24px = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData camera_alt_24px = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData change_language = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData change_theme = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_bubble_empty_24px = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_bubble_24px = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chat_seetings_24px = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData check_24px = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData chevron_left_24px = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData clear_24px = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData content_copy_24px = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData create_group = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData cwtch_knott = IconData(0xe811, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dark_mode_24px = IconData(0xe812, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData delete_24px = IconData(0xe813, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData dns_24px = IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData drag_indicator_24px = IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData edit_24px = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData enable_experiments = IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData enable_groups = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_closed = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData eye_open = IconData(0xe81a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_24dp = IconData(0xe81b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_broken = IconData(0xe81c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_brokenhalf = IconData(0xe81d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_malformed = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData favorite_black_24dp_sad = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData group_add_24px = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData group_settings_24px = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData groups_24px = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData info_24px = IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData join_group = IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData list_black_24dp = IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lock_open_24px = IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lock_24px = IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData maps_ugc_24px = IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData menu_24px = IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData message_24px = IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData mood_24px = IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData more_vert_24px = IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData negative_heart_24px = IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_off = IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_on = IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData onion_waiting = IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData peer_history = IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData peer_settings_24px = IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_add_alt_1_24px = IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_add_24px = IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData person_24px = IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData push_pin_black_24dp = IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData push_pin_24px = IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData search_24px = IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData send_24px = IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData settings_24px = IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_4_bar_24px = IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_alt_24px = IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_connected_no_internet_4_bar_24px = IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData signal_cellular_off_24px = IconData(0xe83e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData swap_horiz_24px = IconData(0xe83f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_disabled_24px = IconData(0xe840, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_problem_24px = IconData(0xe841, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData sync_24px = IconData(0xe842, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_01 = IconData(0xe843, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_02 = IconData(0xe844, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData syncing_03 = IconData(0xe845, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData toggle_on_24px = IconData(0xe846, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData vpn_key_24px = IconData(0xe847, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_blocked = IconData(0xe848, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px = IconData(0xe849, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_lines = IconData(0xe84a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_lines_thin___blocked = IconData(0xe84b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData account_circle_24px_user = IconData(0xe84c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_circle_24px = IconData(0xe84d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_group = IconData(0xe84e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_peer = IconData(0xe84f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData add_24px = IconData(0xe850, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData address_copy_2 = IconData(0xe852, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData address = IconData(0xe856, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData send_invite = IconData(0xe888, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData leave_group = IconData(0xe88a, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData leave_chat = IconData(0xe88b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@ -17,10 +17,6 @@ class ErrorHandler extends ChangeNotifier {
bool importBundleError = false; bool importBundleError = false;
bool importBundleSuccess = false; bool importBundleSuccess = false;
static const String deleteProfileErrorPrefix = "deleteprofile";
bool deleteProfileError = false;
bool deleteProfileSuccess = false;
/// Called by the event bus. /// Called by the event bus.
handleUpdate(String error) { handleUpdate(String error) {
var parts = error.split("."); var parts = error.split(".");
@ -33,10 +29,6 @@ class ErrorHandler extends ChangeNotifier {
break; break;
case importBundleErrorPrefix: case importBundleErrorPrefix:
handleImportBundleError(errorType); handleImportBundleError(errorType);
break;
case deleteProfileErrorPrefix:
handleDeleteProfileError(errorType);
break;
} }
notifyListeners(); notifyListeners();
@ -57,7 +49,6 @@ class ErrorHandler extends ChangeNotifier {
break; break;
case successErrorType: case successErrorType:
explicitAddContactSuccess = true; explicitAddContactSuccess = true;
importBundleSuccess = true;
break; break;
} }
} }
@ -76,19 +67,4 @@ class ErrorHandler extends ChangeNotifier {
break; break;
} }
} }
handleDeleteProfileError(String errorType) {
// Reset add contact errors
deleteProfileError = false;
deleteProfileSuccess = false;
switch (errorType) {
case successErrorType:
deleteProfileSuccess = true;
break;
default:
deleteProfileError = true;
break;
}
}
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Annehmen",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "bestätigt",
"serverNotSynced": "Syncing New Messages (This can take some time)...", "addListItem": "Liste hinzufügen",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "Element hinzufügen",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Ein neues Element zur Liste hinzufügen",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "Neues Profil hinzufügen",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "Peer hinzufügen",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "Einen Peer hinzufügen",
"malformedMessage": "Malformed message", "addProfileTitle": "Neues Profil hinzufügen",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Adresse",
"debugLog": "Turn on console debug logging", "blockBtn": "Peer blockieren",
"torNetworkStatus": "Tor network status", "blocked": "Blockiert",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "Unbekannte Peers blockieren",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "Aufgebaut auf: %2",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Meldungen",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Chat",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "",
"sendInvite": "Send a contact or group invite", "conversationSettings": "",
"sendMessage": "Send Message", "copiedClipboardNotification": "in die Zwischenablage kopiert",
"cancel": "Cancel", "copiedToClipboardNotification": "in die Zwischenablage kopiert",
"resetTor": "Reset", "copyBtn": "Kopieren",
"torStatus": "Tor Status", "couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
"torVersion": "Tor Version", "createGroup": "Gruppe erstellen",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Anlegen",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "Eine Gruppe erstellen",
"rejected": "Rejected!", "createGroupTitle": "Gruppe Anlegen",
"accepted": "Accepted!", "createProfileBtn": "Profil speichern",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "derzeitiges Passwort",
"newPassword": "New Password", "cwtchSettingsTitle": "Cwtch Einstellungen",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "",
"pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen", "cycleMorphsAndroid": "",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "",
"titleManageContacts": "Conversations", "dateDaysAgo": "",
"titleManageServers": "Manage Servers", "dateHoursAgo": "",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "",
"dateNever": "Never", "dateLastYear": "",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "",
"dateLastYear": "Last Year", "dateMonthsAgo": "",
"dateYesterday": "Yesterday", "dateNever": "",
"dateLastMonth": "Last Month", "dateRightNow": "",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "",
"dateHoursAgo": "Hours Ago", "dateYesterday": "",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "Tolle Gruppe",
"dateRightNow": "Right Now", "defaultProfileName": "Alice",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Löschen",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "LÖSCHEN",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "Profil löschen",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "Profil wirklich löschen",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "",
"invalidImportString": "Invalid import string", "descriptionExperiments": "",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Angezeigter Name",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Klicken, um DM zu senden",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "Peer-Verlauf löschen",
"experimentsEnabled": "Experimente aktiviert", "editProfile": "Profil bearbeiten",
"localeIt": "Italiana", "editProfileTitle": "Profil bearbeiten",
"localeEs": "Espanol", "enableGroups": "",
"addListItem": "Liste hinzufügen", "enterCurrentPasswordForDelete": "",
"addNewItem": "Ein neues Element zur Liste hinzufügen", "enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen",
"todoPlaceholder": "noch zu erledigen", "error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen",
"newConnectionPaneTitle": "Neue Verbindung", "experimentsEnabled": "Experimente aktiviert",
"networkStatusOnline": "Online", "groupAddr": "Adresse",
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...", "groupName": "Gruppenname",
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen", "groupNameLabel": "Gruppenname",
"networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung", "invalidImportString": "",
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen", "invitation": "Einladung",
"loadingTor": "Tor wird geladen...", "invitationLabel": "Einladung",
"smallTextLabel": "Klein", "inviteBtn": "Einladen",
"defaultScalingText": "defaultmäßige Textgröße (Skalierungsfaktor:", "inviteToGroupLabel": "In die Gruppe einladen",
"builddate": "Aufgebaut auf: %2", "joinGroup": "Gruppe beitreten",
"version": "Version %1", "joinGroupTab": "Einer Gruppe beitreten",
"versionTor": "Version %1 mit tor %2", "largeTextLabel": "Groß",
"themeDark": "Dunkel", "listsBtn": "Listen",
"themeLight": "Licht", "loadingTor": "Tor wird geladen...",
"settingTheme": "Thema", "localeDe": "Deutsche",
"largeTextLabel": "Groß", "localeEn": "",
"settingInterfaceZoom": "Zoomstufe", "localeEs": "",
"localeDe": "Deutsche", "localeFr": "",
"localePt": "Portuguesa", "localeIt": "",
"localeFr": "Frances", "localePt": "",
"localeEn": "English", "membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.",
"settingLanguage": "Sprache", "networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
"blockUnknownLabel": "Unbekannte Peers blockieren", "networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)", "networkStatusDisconnected": "Vom Internet getrennt, überprüfen Sie Ihre Verbindung",
"versionBuilddate": "Version: %1 Aufgebaut auf: %2", "networkStatusOnline": "Online",
"cwtchSettingsTitle": "Cwtch Einstellungen", "newBulletinLabel": "Neue Meldung",
"unlock": "Entsperren", "newConnectionPaneTitle": "Neue Verbindung",
"yourServers": "Ihre Server", "newGroupBtn": "Neue Gruppe anlegen",
"yourProfiles": "Ihre Profile", "newProfile": "Neues Profil",
"error0ProfilesLoadedForPassword": "0 Profile mit diesem Passwort geladen", "noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"password": "Passwort", "password": "Passwort",
"enterProfilePassword": "Geben Sie ein Passwort ein, um Ihre Profile anzuzeigen", "password1Label": "Passwort",
"addNewProfileBtn": "Neues Profil hinzufügen", "password2Label": "Passwort erneut eingeben",
"deleteConfirmText": "LÖSCHEN", "passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt",
"deleteProfileConfirmBtn": "Profil wirklich löschen", "passwordErrorEmpty": "Passwort kann nicht leer sein",
"deleteConfirmLabel": "Geben Sie LÖSCHEN zur Bestätigung ein", "passwordErrorMatch": "Passwörter stimmen nicht überein",
"deleteProfileBtn": "Profil löschen", "pasteAddressToAddContact": "Adresse hier hinzufügen, um einen Kontakt aufzunehmen",
"passwordChangeError": "Fehler beim Ändern des Passworts: Das Passwort wurde abgelehnt", "peerAddress": "Adresse",
"passwordErrorMatch": "Passwörter stimmen nicht überein", "peerBlockedMessage": "Peer ist blockiert",
"saveProfileBtn": "Profil speichern", "peerName": "Namen",
"createProfileBtn": "Profil speichern", "peerNotOnline": "",
"passwordErrorEmpty": "Passwort kann nicht leer sein", "peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden",
"password2Label": "Passwort erneut eingeben", "pendingLabel": "Bestätigung ausstehend",
"password1Label": "Passwort", "postNewBulletinLabel": "Neue Meldung veröffentlichen",
"currentPasswordLabel": "derzeitiges Passwort", "profileName": "Anzeigename",
"yourDisplayName": "Ihr Anzeigename", "profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten", "puzzleGameBtn": "Puzzlespiel",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", "radioNoPassword": "Unverschlüsselt (kein Passwort)",
"radioNoPassword": "Unverschlüsselt (kein Passwort)", "radioUsePassword": "Passwort",
"radioUsePassword": "Passwort", "rejectGroupBtn": "Ablehnen",
"copiedToClipboardNotification": "in die Zwischenablage kopiert", "saveBtn": "Speichern",
"copyBtn": "Kopieren", "savePeerHistory": "Peer-Verlauf speichern",
"editProfile": "Profil bearbeiten", "savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"newProfile": "Neues Profil", "saveProfileBtn": "Profil speichern",
"defaultProfileName": "Alice", "search": "Suche...",
"profileName": "Anzeigename", "searchList": "",
"editProfileTitle": "Profil bearbeiten", "server": "Server",
"addProfileTitle": "Neues Profil hinzufügen", "serverConnectivityConnected": "Server verbunden",
"deleteBtn": "löschen", "serverConnectivityDisconnected": "Server getrennt",
"unblockBtn": "Peer entblockieren", "serverInfo": "Server-Informationen",
"dontSavePeerHistory": "Peer-Verlauf löschen", "serverLabel": "Server",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem Peer verknüpfter Verlauf gelöscht werden soll oder nicht.", "serverNotSynced": "",
"savePeerHistory": "Peer-Verlauf speichern", "serverSynced": "",
"blockBtn": "Peer blockieren", "settingInterfaceZoom": "Zoomstufe",
"saveBtn": "speichern", "settingLanguage": "Sprache",
"displayNameLabel": "Angezeigter Name", "settingTheme": "Thema",
"addressLabel": "Adresse", "smallTextLabel": "Klein",
"puzzleGameBtn": "Puzzlespiel", "successfullAddedContact": "",
"bulletinsBtn": "Meldungen", "themeDark": "Dunkel",
"listsBtn": "Listen", "themeLight": "Licht",
"chatBtn": "Chat", "titleManageContacts": "",
"rejectGroupBtn": "Ablehnen", "titleManageProfiles": "",
"acceptGroupBtn": "Annehmen", "titleManageServers": "",
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen", "titlePlaceholder": "Titel...",
"newGroupBtn": "Neue Gruppe anlegen", "todoPlaceholder": "noch zu erledigen",
"copiedClipboardNotification": "in die Zwischenablage kopiert", "tooltipAddContact": "",
"peerOfflineMessage": "Peer ist offline, Nachrichten können derzeit nicht zugestellt werden", "tooltipOpenSettings": "",
"peerBlockedMessage": "Peer ist blockiert", "tooltipUnlockProfiles": "",
"pendingLabel": "Bestätigung ausstehend", "unblockBtn": "Peer entblockieren",
"acknowledgedLabel": "bestätigt", "unlock": "Entsperren",
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden", "update": "",
"dmTooltip": "Klicken, um DM zu senden", "version": "Version %1",
"membershipDescription": "Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben.", "versionBuilddate": "Version: %1 Aufgebaut auf: %2",
"addListItemBtn": "Element hinzufügen", "versionTor": "Version %1 mit tor %2",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.", "viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
"searchList": "Search List", "viewServerInfo": "",
"update": "Update", "yourDisplayName": "Ihr Anzeigename",
"inviteBtn": "Einladen", "yourProfiles": "Ihre Profile",
"inviteToGroupLabel": "In die Gruppe einladen", "yourServers": "Ihre Server",
"groupNameLabel": "Gruppenname", "zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)"
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server getrennt",
"serverConnectivityConnected": "Server verbunden",
"serverInfo": "Server-Informationen",
"invitationLabel": "Einladung",
"serverLabel": "Server",
"search": "Suche...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blockiert",
"titlePlaceholder": "Titel...",
"postNewBulletinLabel": "Neue Meldung veröffentlichen",
"newBulletinLabel": "Neue Meldung",
"joinGroup": "Gruppe beitreten",
"createGroup": "Gruppe erstellen",
"addPeer": "Peer hinzufügen",
"groupAddr": "Adresse",
"invitation": "Einladung",
"server": "Server",
"groupName": "Gruppenname",
"peerName": "Namen",
"peerAddress": "Adresse",
"joinGroupTab": "Einer Gruppe beitreten",
"createGroupTab": "Eine Gruppe erstellen",
"addPeerTab": "Einen Peer hinzufügen",
"createGroupBtn": "Anlegen",
"defaultGroupName": "Tolle Gruppe",
"createGroupTitle": "Gruppe Anlegen"
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Accept",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "Do you want to accept the invitation to",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "Acknowledged",
"serverNotSynced": "Syncing New Messages (This can take some time)...", "addListItem": "Add a New List Item",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "Add Item",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Add a new item to the list",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "Add new profile",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "Add Peer",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "Add a peer",
"malformedMessage": "Malformed message", "addProfileTitle": "Add new profile",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Address",
"debugLog": "Turn on console debug logging", "blockBtn": "Block Peer",
"torNetworkStatus": "Tor network status", "blocked": "Blocked",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "Block Unknown Peers",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "Built on: %2",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Bulletins",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Chat",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "Contact Already Exists",
"sendInvite": "Send a contact or group invite", "conversationSettings": "Conversation Settings",
"sendMessage": "Send Message", "copiedClipboardNotification": "Copied to clipboard",
"cancel": "Cancel", "copiedToClipboardNotification": "Copied to Clipboard",
"resetTor": "Reset", "copyBtn": "Copy",
"torStatus": "Tor Status", "couldNotSendMsgError": "Could not send this message",
"torVersion": "Tor Version", "createGroup": "Create group",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Create",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "Create a group",
"rejected": "Rejected!", "createGroupTitle": "Create Group",
"accepted": "Accepted!", "createProfileBtn": "Create Profile",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "Current Password",
"newPassword": "New Password", "cwtchSettingsTitle": "Cwtch Settings",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "Click to cycle category.\\nLong-press to reset.",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "Click to cycle category.\\nRight-click to reset.",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "Click to cycle colours.\\nLong-press to reset.",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "Click to cycle colours.\\nRight-click to reset.",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", "cycleMorphsAndroid": "Click to cycle morphs.\\nLong-press to reset.",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "Click to cycle morphs.\\nRight-click to reset.",
"titleManageContacts": "Conversations", "dateDaysAgo": "Days Ago",
"titleManageServers": "Manage Servers", "dateHoursAgo": "Hours Ago",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "Last Month",
"dateNever": "Never", "dateLastYear": "Last Year",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "Minutes Ago",
"dateLastYear": "Last Year", "dateMonthsAgo": "Months Ago",
"dateYesterday": "Yesterday", "dateNever": "Never",
"dateLastMonth": "Last Month", "dateRightNow": "Right Now",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "Weeks Ago",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)",
"dateHoursAgo": "Hours Ago", "dateYesterday": "Yesterday",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "Awesome Group",
"dateRightNow": "Right Now", "defaultProfileName": "Alice",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "Default size text (scale factor:",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Delete",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "Type DELETE to confirm",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "DELETE",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "Delete Profile",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "Really Delete Profile",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.",
"invalidImportString": "Invalid import string", "descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Display Name",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Click to DM",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "Delete Peer History",
"experimentsEnabled": "Enable Experiments", "editProfile": "Edit Profille",
"localeIt": "Italiana", "editProfileTitle": "Edit Profile",
"localeEs": "Espanol", "enableGroups": "Enable Group Chat",
"addListItem": "Add a New List Item", "enterCurrentPasswordForDelete": "Please enter current password to delete this profile.",
"addNewItem": "Add a new item to the list", "enterProfilePassword": "Enter a password to view your profiles",
"todoPlaceholder": "Todo...", "error0ProfilesLoadedForPassword": "0 profiles loaded with that password",
"newConnectionPaneTitle": "New Connection", "experimentsEnabled": "Enable Experiments",
"networkStatusOnline": "Online", "groupAddr": "Address",
"networkStatusConnecting": "Connecting to network and peers...", "groupName": "Group name",
"networkStatusAttemptingTor": "Attempting to connect to Tor network", "groupNameLabel": "Group Name",
"networkStatusDisconnected": "Disconnected from the internet, check your connection", "invalidImportString": "Invalid import string",
"viewGroupMembershipTooltip": "View Group Membership", "invitation": "Invitation",
"loadingTor": "Loading tor...", "invitationLabel": "Invitation",
"smallTextLabel": "Small", "inviteBtn": "Invite",
"defaultScalingText": "Default size text (scale factor:", "inviteToGroupLabel": "Invite to group",
"builddate": "Built on: %2", "joinGroup": "Join group",
"version": "Version %1", "joinGroupTab": "Join a group",
"versionTor": "Version %1 with tor %2", "largeTextLabel": "Large",
"themeDark": "Dark", "listsBtn": "Lists",
"themeLight": "Light", "loadingTor": "Loading tor...",
"settingTheme": "Theme", "localeDe": "Deutsche",
"largeTextLabel": "Large", "localeEn": "English",
"settingInterfaceZoom": "Zoom level", "localeEs": "Espanol",
"localeDe": "Deutsche", "localeFr": "Frances",
"localePt": "Portuguesa", "localeIt": "Italiana",
"localeFr": "Frances", "localePt": "Portuguesa",
"localeEn": "English", "membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.",
"settingLanguage": "Language", "networkStatusAttemptingTor": "Attempting to connect to Tor network",
"blockUnknownLabel": "Block Unknown Peers", "networkStatusConnecting": "Connecting to network and peers...",
"zoomLabel": "Interface zoom (mostly affects text and button sizes)", "networkStatusDisconnected": "Disconnected from the internet, check your connection",
"versionBuilddate": "Version: %1 Built on: %2", "networkStatusOnline": "Online",
"cwtchSettingsTitle": "Cwtch Settings", "newBulletinLabel": "New Bulletin",
"unlock": "Unlock", "newConnectionPaneTitle": "New Connection",
"yourServers": "Your Servers", "newGroupBtn": "Create new group",
"yourProfiles": "Your Profiles", "newProfile": "New Profile",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password", "noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
"password": "Password", "password": "Password",
"enterProfilePassword": "Enter a password to view your profiles", "password1Label": "Password",
"addNewProfileBtn": "Add new profile", "password2Label": "Reenter password",
"deleteConfirmText": "DELETE", "passwordChangeError": "Error changing password: Supplied password rejected",
"deleteProfileConfirmBtn": "Really Delete Profile", "passwordErrorEmpty": "Password cannot be empty",
"deleteConfirmLabel": "Type DELETE to confirm", "passwordErrorMatch": "Passwords do not match",
"deleteProfileBtn": "Delete Profile", "pasteAddressToAddContact": "Paste a cwtch address here to add a new contact.",
"passwordChangeError": "Error changing password: Supplied password rejected", "peerAddress": "Address",
"passwordErrorMatch": "Passwords do not match", "peerBlockedMessage": "Peer is blocked",
"saveProfileBtn": "Save Profile", "peerName": "Name",
"createProfileBtn": "Create Profile", "peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
"passwordErrorEmpty": "Password cannot be empty", "peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
"password2Label": "Reenter password", "pendingLabel": "Pending",
"password1Label": "Password", "postNewBulletinLabel": "Post new bulletin",
"currentPasswordLabel": "Current Password", "profileName": "Display name",
"yourDisplayName": "Your Display Name", "profileOnionLabel": "Send this address to peers you want to connect with",
"profileOnionLabel": "Send this address to peers you want to connect with", "puzzleGameBtn": "Puzzle Game",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", "radioNoPassword": "Unencrypted (No password)",
"radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "Password",
"radioUsePassword": "Password", "rejectGroupBtn": "Reject",
"copiedToClipboardNotification": "Copied to Clipboard", "saveBtn": "Save",
"copyBtn": "Copy", "savePeerHistory": "Save Peer History",
"editProfile": "Edit Profille", "savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
"newProfile": "New Profile", "saveProfileBtn": "Save Profile",
"defaultProfileName": "Alice", "search": "Search...",
"profileName": "Display name", "searchList": "Search List",
"editProfileTitle": "Edit Profile", "server": "Server",
"addProfileTitle": "Add new profile", "serverConnectivityConnected": "Server Connected",
"deleteBtn": "Delete", "serverConnectivityDisconnected": "Server Disconnected",
"unblockBtn": "Unblock Peer", "serverInfo": "Server Information",
"dontSavePeerHistory": "Delete Peer History", "serverLabel": "Server",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", "serverNotSynced": "Out of Sync",
"savePeerHistory": "Save Peer History", "serverSynced": "Synced",
"blockBtn": "Block Peer", "settingInterfaceZoom": "Zoom level",
"saveBtn": "Save", "settingLanguage": "Language",
"displayNameLabel": "Display Name", "settingTheme": "Theme",
"addressLabel": "Address", "smallTextLabel": "Small",
"puzzleGameBtn": "Puzzle Game", "successfullAddedContact": "Successfully added ",
"bulletinsBtn": "Bulletins", "themeDark": "Dark",
"listsBtn": "Lists", "themeLight": "Light",
"chatBtn": "Chat", "titleManageContacts": "Manage Contacts",
"rejectGroupBtn": "Reject", "titleManageProfiles": "Manage Cwtch Profiles",
"acceptGroupBtn": "Accept", "titleManageServers": "Manage Servers",
"acceptGroupInviteLabel": "Do you want to accept the invitation to", "titlePlaceholder": "title...",
"newGroupBtn": "Create new group", "todoPlaceholder": "Todo...",
"copiedClipboardNotification": "Copied to clipboard", "tooltipAddContact": "Add a new contact",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now", "tooltipOpenSettings": "Open the settings pane",
"peerBlockedMessage": "Peer is blocked", "tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.",
"pendingLabel": "Pending", "unblockBtn": "Unblock Peer",
"acknowledgedLabel": "Acknowledged", "unlock": "Unlock",
"couldNotSendMsgError": "Could not send this message", "update": "Update",
"dmTooltip": "Click to DM", "version": "Version %1",
"membershipDescription": "Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group.", "versionBuilddate": "Version: %1 Built on: %2",
"addListItemBtn": "Add Item", "versionTor": "Version %1 with tor %2",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.", "viewGroupMembershipTooltip": "View Group Membership",
"searchList": "Search List", "viewServerInfo": "Server Info",
"update": "Update", "yourDisplayName": "Your Display Name",
"inviteBtn": "Invite", "yourProfiles": "Your Profiles",
"inviteToGroupLabel": "Invite to group", "yourServers": "Your Servers",
"groupNameLabel": "Group Name", "zoomLabel": "Interface zoom (mostly affects text and button sizes)"
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Invitation",
"serverLabel": "Server",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "title...",
"postNewBulletinLabel": "Post new bulletin",
"newBulletinLabel": "New Bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Create",
"defaultGroupName": "Awesome Group",
"createGroupTitle": "Create Group"
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Aceptar",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "Reconocido",
"serverNotSynced": "Fuera de sincronización con el servidor", "addListItem": "Añadir un nuevo elemento a la lista",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "Agregar artículo",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Añadir un nuevo elemento a la lista",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "Agregar nuevo perfil",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "Agregar Contacto",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "Agregar Contacto",
"malformedMessage": "Malformed message", "addProfileTitle": "Agregar nuevo perfil",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Dirección",
"debugLog": "Turn on console debug logging", "blockBtn": "Bloquear contacto",
"torNetworkStatus": "Tor network status", "blocked": "Bloqueado",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "Bloquear conexiones desconocidas",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "Basado en: %2",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Boletines",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Chat",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "",
"sendInvite": "Send a contact or group invite", "conversationSettings": "",
"sendMessage": "Send Message", "copiedClipboardNotification": "Copiado al portapapeles",
"cancel": "Cancel", "copiedToClipboardNotification": "Copiado al portapapeles",
"resetTor": "Reset", "copyBtn": "Copiar",
"torStatus": "Tor Status", "couldNotSendMsgError": "No se pudo enviar este mensaje",
"torVersion": "Tor Version", "createGroup": "Crear perfil",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Crear",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "Crear un grupo",
"rejected": "Rejected!", "createGroupTitle": "Crear un grupo",
"accepted": "Accepted!", "createProfileBtn": "Crear perfil",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "Contraseña actual",
"newPassword": "New Password", "cwtchSettingsTitle": "Configuración de Cwtch",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...", "cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"titleManageContacts": "Conversations", "dateDaysAgo": "",
"titleManageServers": "Manage Servers", "dateHoursAgo": "",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "",
"dateNever": "Never", "dateLastYear": "",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "",
"dateLastYear": "Last Year", "dateMonthsAgo": "",
"dateYesterday": "Yesterday", "dateNever": "",
"dateLastMonth": "Last Month", "dateRightNow": "",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "",
"dateHoursAgo": "Hours Ago", "dateYesterday": "",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "El Grupo Asombroso",
"dateRightNow": "Right Now", "defaultProfileName": "Alicia",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "Tamaño predeterminado de texto (factor de escala:",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Eliminar",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "Escribe ELIMINAR para confirmar",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "ELIMINAR",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "Eliminar Perfil",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "Confirmar eliminar perfil",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "",
"invalidImportString": "Invalid import string", "descriptionExperiments": "",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Nombre de Usuario",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Haz clic para enviar mensaje directo",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "Eliminar historial de contacto",
"experimentsEnabled": "Experimentos habilitados", "editProfile": "Editar perfil",
"localeIt": "Italiano", "editProfileTitle": "Editar perfil",
"localeEs": "Español", "enableGroups": "",
"addListItem": "Añadir un nuevo elemento a la lista", "enterCurrentPasswordForDelete": "",
"addNewItem": "Añadir un nuevo elemento a la lista", "enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles",
"todoPlaceholder": "Por hacer...", "error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña",
"newConnectionPaneTitle": "Nueva conexión", "experimentsEnabled": "Experimentos habilitados",
"networkStatusOnline": "En línea", "groupAddr": "Dirección",
"networkStatusConnecting": "Conectando a la red y a los contactos...", "groupName": "Nombre del grupo",
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor", "groupNameLabel": "Nombre del grupo",
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión", "invalidImportString": "",
"viewGroupMembershipTooltip": "Ver membresía del grupo", "invitation": "Invitación",
"loadingTor": "Cargando tor...", "invitationLabel": "Invitación",
"smallTextLabel": "Pequeño", "inviteBtn": "Invitar",
"defaultScalingText": "Tamaño predeterminado de texto (factor de escala:", "inviteToGroupLabel": "Invitar al grupo",
"builddate": "Basado en: %2", "joinGroup": "Únete al grupo",
"version": "Versión %1", "joinGroupTab": "Únete a un grupo",
"versionTor": "Versión %1 con tor %2", "largeTextLabel": "Grande",
"themeDark": "Oscuro", "listsBtn": "Listas",
"themeLight": "Claro", "loadingTor": "Cargando tor...",
"settingTheme": "Tema", "localeDe": "Alemán",
"largeTextLabel": "Grande", "localeEn": "Inglés",
"settingInterfaceZoom": "Nivel de zoom", "localeEs": "Español",
"localeDe": "Alemán", "localeFr": "Francés",
"localePt": "Portugués", "localeIt": "Italiano",
"localeFr": "Francés", "localePt": "Portugués",
"localeEn": "Inglés", "membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo",
"settingLanguage": "Idioma", "networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
"blockUnknownLabel": "Bloquear conexiones desconocidas", "networkStatusConnecting": "Conectando a la red y a los contactos...",
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)", "networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
"versionBuilddate": "Versión: %1 Basado en %2", "networkStatusOnline": "En línea",
"cwtchSettingsTitle": "Configuración de Cwtch", "newBulletinLabel": "Nuevo Boletín",
"unlock": "Desbloquear", "newConnectionPaneTitle": "Nueva conexión",
"yourServers": "Tus servidores", "newGroupBtn": "Crear un nuevo grupo de chat",
"yourProfiles": "Tus perfiles", "newProfile": "Nuevo perfil",
"error0ProfilesLoadedForPassword": "0 perfiles cargados con esa contraseña", "noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados",
"password": "Contraseña", "password": "Contraseña",
"enterProfilePassword": "Ingresa tu contraseña para ver tus perfiles", "password1Label": "Contraseña",
"addNewProfileBtn": "Agregar nuevo perfil", "password2Label": "Vuelve a ingresar tu contraseña",
"deleteConfirmText": "ELIMINAR", "passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada",
"deleteProfileConfirmBtn": "Confirmar eliminar perfil", "passwordErrorEmpty": "El campo de contraseña no puede estar vacío",
"deleteConfirmLabel": "Escribe ELIMINAR para confirmar", "passwordErrorMatch": "Las contraseñas no coinciden",
"deleteProfileBtn": "Eliminar Perfil", "pasteAddressToAddContact": "...pegar una dirección aquí para añadir contacto...",
"passwordChangeError": "Hubo un error cambiando tu contraseña: la contraseña ingresada fue rechazada", "peerAddress": "Dirección",
"passwordErrorMatch": "Las contraseñas no coinciden", "peerBlockedMessage": "Contacto bloqueado",
"saveProfileBtn": "Guardar perfil", "peerName": "Nombre",
"createProfileBtn": "Crear perfil", "peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
"passwordErrorEmpty": "El campo de contraseña no puede estar vacío", "peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
"password2Label": "Vuelve a ingresar tu contraseña", "pendingLabel": "Pendiente",
"password1Label": "Contraseña", "postNewBulletinLabel": "Publicar nuevo boletín",
"currentPasswordLabel": "Contraseña actual", "profileName": "Nombre de Usuario",
"yourDisplayName": "Tu nombre de usuario", "profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte", "puzzleGameBtn": "Juego de rompecabezas",
"noPasswordWarning": "No usar una contraseña para esta cuenta significa que los datos almacenados localmente no serán encriptados", "radioNoPassword": "Sin cifrado (sin contraseña)",
"radioNoPassword": "Sin cifrado (sin contraseña)", "radioUsePassword": "Contraseña",
"radioUsePassword": "Contraseña", "rejectGroupBtn": "Rechazar",
"copiedToClipboardNotification": "Copiado al portapapeles", "saveBtn": "Guardar",
"copyBtn": "Copiar", "savePeerHistory": "Guardar el historial con contacto",
"editProfile": "Editar perfil", "savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
"newProfile": "Nuevo perfil", "saveProfileBtn": "Guardar perfil",
"defaultProfileName": "Alicia", "search": "Búsqueda...",
"profileName": "Nombre de Usuario", "searchList": "Buscar en la lista",
"editProfileTitle": "Editar perfil", "server": "Servidor",
"addProfileTitle": "Agregar nuevo perfil", "serverConnectivityConnected": "Servidor conectado",
"deleteBtn": "Eliminar", "serverConnectivityDisconnected": "Servidor desconectado",
"unblockBtn": "Desbloquear contacto", "serverInfo": "Información del servidor",
"dontSavePeerHistory": "Eliminar historial de contacto", "serverLabel": "Servidor",
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.", "serverNotSynced": "Fuera de sincronización con el servidor",
"savePeerHistory": "Guardar el historial con contacto", "serverSynced": "Sincronizado",
"blockBtn": "Bloquear contacto", "settingInterfaceZoom": "Nivel de zoom",
"saveBtn": "Guardar", "settingLanguage": "Idioma",
"displayNameLabel": "Nombre de Usuario", "settingTheme": "Tema",
"addressLabel": "Dirección", "smallTextLabel": "Pequeño",
"puzzleGameBtn": "Juego de rompecabezas", "successfullAddedContact": "",
"bulletinsBtn": "Boletines", "themeDark": "Oscuro",
"listsBtn": "Listas", "themeLight": "Claro",
"chatBtn": "Chat", "titleManageContacts": "",
"rejectGroupBtn": "Rechazar", "titleManageProfiles": "",
"acceptGroupBtn": "Aceptar", "titleManageServers": "",
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ", "titlePlaceholder": "título...",
"newGroupBtn": "Crear un nuevo grupo de chat", "todoPlaceholder": "Por hacer...",
"copiedClipboardNotification": "Copiado al portapapeles", "tooltipAddContact": "",
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento", "tooltipOpenSettings": "",
"peerBlockedMessage": "Contacto bloqueado", "tooltipUnlockProfiles": "",
"pendingLabel": "Pendiente", "unblockBtn": "Desbloquear contacto",
"acknowledgedLabel": "Reconocido", "unlock": "Desbloquear",
"couldNotSendMsgError": "No se pudo enviar este mensaje", "update": "Actualizar",
"dmTooltip": "Haz clic para enviar mensaje directo", "version": "Versión %1",
"membershipDescription": "La lista a continuación solo muestra los miembros que han enviado mensajes al grupo, no incluye a todos los usuarios dentro del grupo", "versionBuilddate": "Versión: %1 Basado en %2",
"addListItemBtn": "Agregar artículo", "versionTor": "Versión %1 con tor %2",
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento", "viewGroupMembershipTooltip": "Ver membresía del grupo",
"searchList": "Buscar en la lista", "viewServerInfo": "Información del servidor",
"update": "Actualizar", "yourDisplayName": "Tu nombre de usuario",
"inviteBtn": "Invitar", "yourProfiles": "Tus perfiles",
"inviteToGroupLabel": "Invitar al grupo", "yourServers": "Tus servidores",
"groupNameLabel": "Nombre del grupo", "zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)"
"viewServerInfo": "Información del servidor",
"serverSynced": "Sincronizado",
"serverConnectivityDisconnected": "Servidor desconectado",
"serverConnectivityConnected": "Servidor conectado",
"serverInfo": "Información del servidor",
"invitationLabel": "Invitación",
"serverLabel": "Servidor",
"search": "Búsqueda...",
"cycleColoursDesktop": "Click para cambiar colores. Click derecho para reiniciar.",
"cycleColoursAndroid": "Click para cambiar colores. Mantenga pulsado para reiniciar.",
"cycleMorphsDesktop": "Click para cambiar transformaciones. Click derecho para reiniciar.",
"cycleMorphsAndroid": "Click para cambiar transformaciones. Mantenga pulsado para reiniciar.",
"cycleCatsDesktop": "Click para cambiar categoría. Click derecho para reiniciar.",
"cycleCatsAndroid": "Click para cambiar categoría. Mantenga pulsado para reiniciar.",
"blocked": "Bloqueado",
"titlePlaceholder": "título...",
"postNewBulletinLabel": "Publicar nuevo boletín",
"newBulletinLabel": "Nuevo Boletín",
"joinGroup": "Únete al grupo",
"createGroup": "Crear perfil",
"addPeer": "Agregar Contacto",
"groupAddr": "Dirección",
"invitation": "Invitación",
"server": "Servidor",
"groupName": "Nombre del grupo",
"peerName": "Nombre",
"peerAddress": "Dirección",
"joinGroupTab": "Únete a un grupo",
"createGroupTab": "Crear un grupo",
"addPeerTab": "Agregar Contacto",
"createGroupBtn": "Crear",
"defaultGroupName": "El Grupo Asombroso",
"createGroupTitle": "Crear un grupo"
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Accepter",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "Confirmé",
"serverNotSynced": "Syncing New Messages (This can take some time)...", "addListItem": "Ajouter un nouvel élément",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Ajouter un nouvel élément à la liste",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "",
"malformedMessage": "Malformed message", "addProfileTitle": "",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Adresse",
"debugLog": "Turn on console debug logging", "blockBtn": "",
"torNetworkStatus": "Tor network status", "blocked": "",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Bulletins",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Discuter",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "",
"sendInvite": "Send a contact or group invite", "conversationSettings": "",
"sendMessage": "Send Message", "copiedClipboardNotification": "Copié dans le presse-papier",
"cancel": "Cancel", "copiedToClipboardNotification": "Copié dans le presse-papier",
"resetTor": "Reset", "copyBtn": "Copier",
"torStatus": "Tor Status", "couldNotSendMsgError": "Impossible d'envoyer ce message",
"torVersion": "Tor Version", "createGroup": "",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Créer",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "",
"rejected": "Rejected!", "createGroupTitle": "Créer un groupe",
"accepted": "Accepted!", "createProfileBtn": "",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "",
"newPassword": "New Password", "cwtchSettingsTitle": "Préférences Cwtch",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "",
"pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...", "cycleMorphsAndroid": "",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "",
"titleManageContacts": "Conversations", "dateDaysAgo": "",
"titleManageServers": "Manage Servers", "dateHoursAgo": "",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "",
"dateNever": "Never", "dateLastYear": "",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "",
"dateLastYear": "Last Year", "dateMonthsAgo": "",
"dateYesterday": "Yesterday", "dateNever": "",
"dateLastMonth": "Last Month", "dateRightNow": "",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "",
"dateHoursAgo": "Hours Ago", "dateYesterday": "",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "Un super groupe",
"dateRightNow": "Right Now", "defaultProfileName": "",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "Taille par défaut du texte (échelle:",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Effacer",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "",
"invalidImportString": "Invalid import string", "descriptionExperiments": "",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Pseudo",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Envoyer un message privé",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "",
"experimentsEnabled": "Enable Experiments", "editProfile": "",
"localeIt": "Italiana", "editProfileTitle": "",
"localeEs": "Espanol", "enableGroups": "",
"addListItem": "Ajouter un nouvel élément", "enterCurrentPasswordForDelete": "",
"addNewItem": "Ajouter un nouvel élément à la liste", "enterProfilePassword": "",
"todoPlaceholder": "A faire...", "error0ProfilesLoadedForPassword": "",
"newConnectionPaneTitle": "New Connection", "experimentsEnabled": "",
"networkStatusOnline": "Online", "groupAddr": "",
"networkStatusConnecting": "Connecting to network and peers...", "groupName": "",
"networkStatusAttemptingTor": "Attempting to connect to Tor network", "groupNameLabel": "Nom du groupe",
"networkStatusDisconnected": "Disconnected from the internet, check your connection", "invalidImportString": "",
"viewGroupMembershipTooltip": "View Group Membership", "invitation": "",
"loadingTor": "Loading tor...", "invitationLabel": "Invitation",
"smallTextLabel": "Petit", "inviteBtn": "Invitation",
"defaultScalingText": "Taille par défaut du texte (échelle:", "inviteToGroupLabel": "Inviter quelqu'un",
"builddate": "Built on: %2", "joinGroup": "",
"version": "Version %1", "joinGroupTab": "",
"versionTor": "Version %1 with tor %2", "largeTextLabel": "Large",
"themeDark": "Dark", "listsBtn": "Listes",
"themeLight": "Light", "loadingTor": "",
"settingTheme": "Theme", "localeDe": "",
"largeTextLabel": "Large", "localeEn": "",
"settingInterfaceZoom": "Zoom level", "localeEs": "",
"localeDe": "Deutsche", "localeFr": "",
"localePt": "Portuguesa", "localeIt": "",
"localeFr": "Frances", "localePt": "",
"localeEn": "English", "membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.",
"settingLanguage": "Language", "networkStatusAttemptingTor": "",
"blockUnknownLabel": "Block Unknown Peers", "networkStatusConnecting": "",
"zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)", "networkStatusDisconnected": "",
"versionBuilddate": "Version: %1 Built on: %2", "networkStatusOnline": "",
"cwtchSettingsTitle": "Préférences Cwtch", "newBulletinLabel": "Nouveau bulletin",
"unlock": "Unlock", "newConnectionPaneTitle": "",
"yourServers": "Your Servers", "newGroupBtn": "Créer un nouveau groupe",
"yourProfiles": "Your Profiles", "newProfile": "",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password", "noPasswordWarning": "",
"password": "Password", "password": "",
"enterProfilePassword": "Enter a password to view your profiles", "password1Label": "",
"addNewProfileBtn": "Add new profile", "password2Label": "",
"deleteConfirmText": "DELETE", "passwordChangeError": "",
"deleteProfileConfirmBtn": "Really Delete Profile", "passwordErrorEmpty": "",
"deleteConfirmLabel": "Type DELETE to confirm", "passwordErrorMatch": "",
"deleteProfileBtn": "Delete Profile", "pasteAddressToAddContact": "... coller une adresse ici pour ajouter un contact...",
"passwordChangeError": "Error changing password: Supplied password rejected", "peerAddress": "",
"passwordErrorMatch": "Passwords do not match", "peerBlockedMessage": "",
"saveProfileBtn": "Save Profile", "peerName": "",
"createProfileBtn": "Create Profile", "peerNotOnline": "",
"passwordErrorEmpty": "Password cannot be empty", "peerOfflineMessage": "",
"password2Label": "Reenter password", "pendingLabel": "En attente",
"password1Label": "Password", "postNewBulletinLabel": "Envoyer un nouveau bulletin",
"currentPasswordLabel": "Current Password", "profileName": "",
"yourDisplayName": "Your Display Name", "profileOnionLabel": "",
"profileOnionLabel": "Send this address to peers you want to connect with", "puzzleGameBtn": "Puzzle",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", "radioNoPassword": "",
"radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "",
"radioUsePassword": "Password", "rejectGroupBtn": "Refuser",
"copiedToClipboardNotification": "Copié dans le presse-papier", "saveBtn": "Sauvegarder",
"copyBtn": "Copier", "savePeerHistory": "",
"editProfile": "Edit Profille", "savePeerHistoryDescription": "",
"newProfile": "New Profile", "saveProfileBtn": "",
"defaultProfileName": "Alice", "search": "",
"profileName": "Display name", "searchList": "",
"editProfileTitle": "Edit Profile", "server": "",
"addProfileTitle": "Add new profile", "serverConnectivityConnected": "",
"deleteBtn": "Effacer", "serverConnectivityDisconnected": "",
"unblockBtn": "Unblock Peer", "serverInfo": "",
"dontSavePeerHistory": "Delete Peer History", "serverLabel": "Serveur",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", "serverNotSynced": "",
"savePeerHistory": "Save Peer History", "serverSynced": "",
"blockBtn": "Block Peer", "settingInterfaceZoom": "",
"saveBtn": "Sauvegarder", "settingLanguage": "",
"displayNameLabel": "Pseudo", "settingTheme": "",
"addressLabel": "Adresse", "smallTextLabel": "Petit",
"puzzleGameBtn": "Puzzle", "successfullAddedContact": "",
"bulletinsBtn": "Bulletins", "themeDark": "",
"listsBtn": "Listes", "themeLight": "",
"chatBtn": "Discuter", "titleManageContacts": "",
"rejectGroupBtn": "Refuser", "titleManageProfiles": "",
"acceptGroupBtn": "Accepter", "titleManageServers": "",
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe", "titlePlaceholder": "titre...",
"newGroupBtn": "Créer un nouveau groupe", "todoPlaceholder": "A faire...",
"copiedClipboardNotification": "Copié dans le presse-papier", "tooltipAddContact": "",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now", "tooltipOpenSettings": "",
"peerBlockedMessage": "Peer is blocked", "tooltipUnlockProfiles": "",
"pendingLabel": "En attente", "unblockBtn": "",
"acknowledgedLabel": "Confirmé", "unlock": "",
"couldNotSendMsgError": "Impossible d'envoyer ce message", "update": "",
"dmTooltip": "Envoyer un message privé", "version": "",
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe.", "versionBuilddate": "",
"addListItemBtn": "Add Item", "versionTor": "",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.", "viewGroupMembershipTooltip": "",
"searchList": "Search List", "viewServerInfo": "",
"update": "Update", "yourDisplayName": "",
"inviteBtn": "Invitation", "yourProfiles": "",
"inviteToGroupLabel": "Inviter quelqu'un", "yourServers": "",
"groupNameLabel": "Nom du groupe", "zoomLabel": "Interface zoom (essentiellement la taille du texte et des composants de l'interface)"
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Invitation",
"serverLabel": "Serveur",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "titre...",
"postNewBulletinLabel": "Envoyer un nouveau bulletin",
"newBulletinLabel": "Nouveau bulletin",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Créer",
"defaultGroupName": "Un super groupe",
"createGroupTitle": "Créer un groupe"
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Accetta",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "Vuoi accettare l'invito a",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "Riconosciuto",
"serverNotSynced": "Non sincronizzato", "addListItem": "Aggiungi un nuovo elemento alla lista",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "Aggiungi elemento",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Aggiungi un nuovo elemento alla lista",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "Aggiungi nuovo profilo",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "Aggiungi peer",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "Aggiungi un peer",
"malformedMessage": "Malformed message", "addProfileTitle": "Aggiungi nuovo profilo",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Indirizzo",
"debugLog": "Turn on console debug logging", "blockBtn": "Blocca il peer",
"torNetworkStatus": "Tor network status", "blocked": "Bloccato",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "Blocca peer sconosciuti",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "Costruito il: %2",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Bollettini",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Chat",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "",
"sendInvite": "Send a contact or group invite", "conversationSettings": "",
"sendMessage": "Send Message", "copiedClipboardNotification": "Copiato negli Appunti",
"cancel": "Cancel", "copiedToClipboardNotification": "Copiato negli Appunti",
"resetTor": "Reset", "copyBtn": "Copia",
"torStatus": "Tor Status", "couldNotSendMsgError": "Impossibile inviare questo messaggio",
"torVersion": "Tor Version", "createGroup": "Crea un gruppo",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Crea",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "Crea un gruppo",
"rejected": "Rejected!", "createGroupTitle": "Crea un gruppo",
"accepted": "Accepted!", "createProfileBtn": "Crea un profilo",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "Password corrente",
"newPassword": "New Password", "cwtchSettingsTitle": "Impostazioni di Cwtch",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "Fare clic per scorrere le categorie.\\nPressione lunga per resettare.",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "Fare clic per scorrere le categorie.\\nCliccare con il tasto destro per resettare.",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "Fare clic per scorrere i colori.\\nPressione lunga per resettare.",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "Fare clic per scorrere i colori.\\nCliccare con il tasto destro per resettare.",
"pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...", "cycleMorphsAndroid": "Fare clic per scorrere i morph.\\nPressione lunga per resettare.",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "Fare clic per scorrere i morph.\\nCliccare con il tasto destro per resettare.",
"titleManageContacts": "Conversations", "dateDaysAgo": "",
"titleManageServers": "Manage Servers", "dateHoursAgo": "",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "",
"dateNever": "Never", "dateLastYear": "",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "",
"dateLastYear": "Last Year", "dateMonthsAgo": "",
"dateYesterday": "Yesterday", "dateNever": "",
"dateLastMonth": "Last Month", "dateRightNow": "",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "",
"dateHoursAgo": "Hours Ago", "dateYesterday": "",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "Gruppo fantastico",
"dateRightNow": "Right Now", "defaultProfileName": "Alice",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Elimina",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "Digita ELIMINA per confermare",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "ELIMINA",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "Elimina profilo",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "Elimina realmente il profilo",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "",
"invalidImportString": "Invalid import string", "descriptionExperiments": "",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Nome visualizzato",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Clicca per inviare un Messagio Diretto",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "Elimina cronologia dei peer",
"experimentsEnabled": "Esperimenti abilitati", "editProfile": "Modifica profilo",
"localeIt": "Italiano", "editProfileTitle": "Modifica profilo",
"localeEs": "Spagnolo", "enableGroups": "",
"addListItem": "Aggiungi un nuovo elemento alla lista", "enterCurrentPasswordForDelete": "",
"addNewItem": "Aggiungi un nuovo elemento alla lista", "enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili",
"todoPlaceholder": "Da fare...", "error0ProfilesLoadedForPassword": "0 profili caricati con quella password",
"newConnectionPaneTitle": "Nuova connessione", "experimentsEnabled": "Esperimenti abilitati",
"networkStatusOnline": "Online", "groupAddr": "Indirizzo",
"networkStatusConnecting": "Connessione alla rete e ai peer ...", "groupName": "Nome del gruppo",
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor", "groupNameLabel": "Nome del gruppo",
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione", "invalidImportString": "",
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo", "invitation": "Invito",
"loadingTor": "Caricamento di tor...", "invitationLabel": "Invito",
"smallTextLabel": "Piccolo", "inviteBtn": "Invitare",
"defaultScalingText": "Testo di dimensioni predefinite (fattore di scala:", "inviteToGroupLabel": "Invitare nel gruppo",
"builddate": "Costruito il: %2", "joinGroup": "Unisciti al gruppo",
"version": "Versione %1", "joinGroupTab": "Unisciti a un gruppo",
"versionTor": "Versione %1 con tor %2", "largeTextLabel": "Grande",
"themeDark": "Scuro", "listsBtn": "Liste",
"themeLight": "Chiaro", "loadingTor": "Caricamento di tor...",
"settingTheme": "Tema", "localeDe": "Tedesco",
"largeTextLabel": "Grande", "localeEn": "Inglese",
"settingInterfaceZoom": "Livello di zoom", "localeEs": "Spagnolo",
"localeDe": "Tedesco", "localeFr": "Francese",
"localePt": "Portoghese", "localeIt": "Italiano",
"localeFr": "Francese", "localePt": "Portoghese",
"localeEn": "Inglese", "membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.",
"settingLanguage": "Lingua", "networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
"blockUnknownLabel": "Blocca peer sconosciuti", "networkStatusConnecting": "Connessione alla rete e ai peer ...",
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)", "networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
"versionBuilddate": "Versione: %1 Costruito il: %2", "networkStatusOnline": "Online",
"cwtchSettingsTitle": "Impostazioni di Cwtch", "newBulletinLabel": "Nuovo bollettino",
"unlock": "Sblocca", "newConnectionPaneTitle": "Nuova connessione",
"yourServers": "I tuoi server", "newGroupBtn": "Crea un nuovo gruppo",
"yourProfiles": "I tuoi profili", "newProfile": "Nuovo profilo",
"error0ProfilesLoadedForPassword": "0 profili caricati con quella password", "noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"password": "Password", "password": "Password",
"enterProfilePassword": "Inserisci una password per visualizzare i tuoi profili", "password1Label": "Password",
"addNewProfileBtn": "Aggiungi nuovo profilo", "password2Label": "Reinserire la password",
"deleteConfirmText": "ELIMINA", "passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata",
"deleteProfileConfirmBtn": "Elimina realmente il profilo", "passwordErrorEmpty": "La password non può essere vuota",
"deleteConfirmLabel": "Digita ELIMINA per confermare", "passwordErrorMatch": "Le password non corrispondono",
"deleteProfileBtn": "Elimina profilo", "pasteAddressToAddContact": "... incolla qui un indirizzo per aggiungere un contatto...",
"passwordChangeError": "Errore durante la modifica della password: password fornita rifiutata", "peerAddress": "Indirizzo",
"passwordErrorMatch": "Le password non corrispondono", "peerBlockedMessage": "Il peer è bloccato",
"saveProfileBtn": "Salva il profilo", "peerName": "Nome",
"createProfileBtn": "Crea un profilo", "peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
"passwordErrorEmpty": "La password non può essere vuota", "peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
"password2Label": "Reinserire la password", "pendingLabel": "In corso",
"password1Label": "Password", "postNewBulletinLabel": "Pubblica un nuovo bollettino",
"currentPasswordLabel": "Password corrente", "profileName": "Nome visualizzato",
"yourDisplayName": "Il tuo nome visualizzato", "profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi", "puzzleGameBtn": "Gioco di puzzle",
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati", "radioNoPassword": "Non criptato (senza password)",
"radioNoPassword": "Non criptato (senza password)", "radioUsePassword": "Password",
"radioUsePassword": "Password", "rejectGroupBtn": "Rifiuta",
"copiedToClipboardNotification": "Copiato negli appunti", "saveBtn": "Salva",
"copyBtn": "Copia", "savePeerHistory": "Salva cronologia peer",
"editProfile": "Modifica profilo", "savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
"newProfile": "Nuovo profilo", "saveProfileBtn": "Salva il profilo",
"defaultProfileName": "Alice", "search": "Ricerca...",
"profileName": "Nome visualizzato", "searchList": "Cerca nella lista",
"editProfileTitle": "Modifica profilo", "server": "Server",
"addProfileTitle": "Aggiungi nuovo profilo", "serverConnectivityConnected": "Server connesso",
"deleteBtn": "Elimina", "serverConnectivityDisconnected": "Server disconnesso",
"unblockBtn": "Sblocca il peer", "serverInfo": "Informazioni sul server",
"dontSavePeerHistory": "Elimina cronologia dei peer", "serverLabel": "Server",
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.", "serverNotSynced": "Non sincronizzato",
"savePeerHistory": "Salva cronologia peer", "serverSynced": "Sincronizzato",
"blockBtn": "Blocca il peer", "settingInterfaceZoom": "Livello di zoom",
"saveBtn": "Salva", "settingLanguage": "Lingua",
"displayNameLabel": "Nome visualizzato", "settingTheme": "Tema",
"addressLabel": "Indirizzo", "smallTextLabel": "Piccolo",
"puzzleGameBtn": "Gioco di puzzle", "successfullAddedContact": "",
"bulletinsBtn": "Bollettini", "themeDark": "Scuro",
"listsBtn": "Liste", "themeLight": "Chiaro",
"chatBtn": "Chat", "titleManageContacts": "",
"rejectGroupBtn": "Rifiuta", "titleManageProfiles": "",
"acceptGroupBtn": "Accetta", "titleManageServers": "",
"acceptGroupInviteLabel": "Vuoi accettare l'invito a", "titlePlaceholder": "titolo...",
"newGroupBtn": "Crea un nuovo gruppo", "todoPlaceholder": "Da fare...",
"copiedClipboardNotification": "Copiato negli Appunti", "tooltipAddContact": "",
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento", "tooltipOpenSettings": "",
"peerBlockedMessage": "Il peer è bloccato", "tooltipUnlockProfiles": "",
"pendingLabel": "In corso", "unblockBtn": "Sblocca il peer",
"acknowledgedLabel": "Riconosciuto", "unlock": "Sblocca",
"couldNotSendMsgError": "Impossibile inviare questo messaggio", "update": "Aggiornamento",
"dmTooltip": "Clicca per inviare un Messagio Diretto", "version": "Versione %1",
"membershipDescription": "Di seguito è riportato un elenco di utenti che hanno inviato messaggi al gruppo. Questo elenco potrebbe non corrispondere a tutti gli utenti che hanno accesso al gruppo.", "versionBuilddate": "Versione: %1 Costruito il: %2",
"addListItemBtn": "Aggiungi elemento", "versionTor": "Versione %1 con tor %2",
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.", "viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
"searchList": "Cerca nella lista", "viewServerInfo": "Informazioni sul server",
"update": "Aggiornamento", "yourDisplayName": "Il tuo nome visualizzato",
"inviteBtn": "Invitare", "yourProfiles": "I tuoi profili",
"inviteToGroupLabel": "Invitare nel gruppo", "yourServers": "I tuoi server",
"groupNameLabel": "Nome del gruppo", "zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)"
"viewServerInfo": "Informazioni sul server",
"serverSynced": "Sincronizzato",
"serverConnectivityDisconnected": "Server disconnesso",
"serverConnectivityConnected": "Server connesso",
"serverInfo": "Informazioni sul server",
"invitationLabel": "Invito",
"serverLabel": "Server",
"search": "Ricerca...",
"cycleColoursDesktop": "Fare clic per scorrere i colori.\nCliccare con il tasto destro per resettare.",
"cycleColoursAndroid": "Fare clic per scorrere i colori.\nPressione lunga per resettare.",
"cycleMorphsDesktop": "Fare clic per scorrere i morph.\nCliccare con il tasto destro per resettare.",
"cycleMorphsAndroid": "Fare clic per scorrere i morph.\nPressione lunga per resettare.",
"cycleCatsDesktop": "Fare clic per scorrere le categorie.\nCliccare con il tasto destro per resettare.",
"cycleCatsAndroid": "Fare clic per scorrere le categorie.\nPressione lunga per resettare.",
"blocked": "Bloccato",
"titlePlaceholder": "titolo...",
"postNewBulletinLabel": "Pubblica un nuovo bollettino",
"newBulletinLabel": "Nuovo bollettino",
"joinGroup": "Unisciti al gruppo",
"createGroup": "Crea un gruppo",
"addPeer": "Aggiungi peer",
"groupAddr": "Indirizzo",
"invitation": "Invito",
"server": "Server",
"groupName": "Nome del gruppo",
"peerName": "Nome",
"peerAddress": "Indirizzo",
"joinGroupTab": "Unisciti a un gruppo",
"createGroupTab": "Crea un gruppo",
"addPeerTab": "Aggiungi un peer",
"createGroupBtn": "Crea",
"defaultGroupName": "Gruppo fantastico",
"createGroupTitle": "Crea un gruppo"
} }

View File

@ -1,193 +1,160 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2021-06-24T23:32:06+02:00", "acceptGroupBtn": "Aceitar",
"tooltipHidePassword": "Hide Password", "acceptGroupInviteLabel": "Você quer aceitar o convite para",
"tooltipShowPassword": "Show Password", "acknowledgedLabel": "Confirmada",
"serverNotSynced": "Syncing New Messages (This can take some time)...", "addListItem": "Adicionar Item à Lista",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.", "addListItemBtn": "",
"shutdownCwtchAction": "Shutdown Cwtch", "addNewItem": "Adicionar novo item à lista",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.", "addNewProfileBtn": "",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?", "addPeer": "",
"shutdownCwtchTooltip": "Shutdown Cwtch", "addPeerTab": "",
"malformedMessage": "Malformed message", "addProfileTitle": "",
"profileDeleteSuccess": "Successfully deleted profile", "addressLabel": "Endereço",
"debugLog": "Turn on console debug logging", "blockBtn": "",
"torNetworkStatus": "Tor network status", "blocked": "",
"addContactFirst": "Add or pick a contact to begin chatting.", "blockUnknownLabel": "",
"createProfileToBegin": "Please create or unlock a profile to begin", "builddate": "",
"nickChangeSuccess": "Profile nickname changed successfully", "bulletinsBtn": "Boletins",
"addServerFirst": "You need to add a server before you can create a group", "chatBtn": "Chat",
"deleteProfileSuccess": "Successfully deleted profile", "contactAlreadyExists": "",
"sendInvite": "Send a contact or group invite", "conversationSettings": "",
"sendMessage": "Send Message", "copiedClipboardNotification": "Copiado",
"cancel": "Cancel", "copiedToClipboardNotification": "Copiado",
"resetTor": "Reset", "copyBtn": "Copiar",
"torStatus": "Tor Status", "couldNotSendMsgError": "Não deu para enviar esta mensagem",
"torVersion": "Tor Version", "createGroup": "",
"sendAnInvitation": "You sent an invitation for: ", "createGroupBtn": "Criar",
"contactSuggestion": "This is a contact suggestion for: ", "createGroupTab": "",
"rejected": "Rejected!", "createGroupTitle": "Criar Grupo",
"accepted": "Accepted!", "createProfileBtn": "",
"chatHistoryDefault": "This conversation will be deleted when Cwtch is closed! Message history can be enabled per-conversation via the Settings menu in the upper right.", "currentPasswordLabel": "",
"newPassword": "New Password", "cwtchSettingsTitle": "Configurações do Cwtch",
"yesLeave": "Yes, Leave This Conversation", "cycleCatsAndroid": "",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "cycleCatsDesktop": "",
"leaveGroup": "Leave This Conversation", "cycleColoursAndroid": "",
"inviteToGroup": "You have been invited to join a group:", "cycleColoursDesktop": "",
"pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…", "cycleMorphsAndroid": "",
"tooltipAddContact": "Add a new contact or conversation", "cycleMorphsDesktop": "",
"titleManageContacts": "Conversations", "dateDaysAgo": "",
"titleManageServers": "Manage Servers", "dateHoursAgo": "",
"dateMonthsAgo": "Months Ago", "dateLastMonth": "",
"dateNever": "Never", "dateLastYear": "",
"dateYearsAgo": "X Years Ago (displayed next to a contact row to indicate time of last action)", "dateMinutesAgo": "",
"dateLastYear": "Last Year", "dateMonthsAgo": "",
"dateYesterday": "Yesterday", "dateNever": "",
"dateLastMonth": "Last Month", "dateRightNow": "",
"dateWeeksAgo": "Weeks Ago", "dateWeeksAgo": "",
"dateDaysAgo": "Days Ago", "dateYearsAgo": "",
"dateHoursAgo": "Hours Ago", "dateYesterday": "",
"dateMinutesAgo": "Minutes Ago", "defaultGroupName": "Grupo incrível",
"dateRightNow": "Right Now", "defaultProfileName": "",
"successfullAddedContact": "Successfully added ", "defaultScalingText": "Texto tamanho padrão (fator de escala: ",
"descriptionBlockUnknownConnections": "If turned on, this option will automatically close connections from Cwtch users that have not been added to your contact list.", "deleteBtn": "Deletar",
"descriptionExperimentsGroups": "The group experiment allows Cwtch to connect with untrusted server infrastructure to facilitate communication with more than one contact.", "deleteConfirmLabel": "",
"descriptionExperiments": "Cwtch experiments are optional, opt-in features that add additional functionality to Cwtch that may have different privacy considerations than traditional 1:1 metadata resistant chat e.g. group chat, bot integration etc.", "deleteConfirmText": "",
"titleManageProfiles": "Manage Cwtch Profiles", "deleteProfileBtn": "",
"tooltipUnlockProfiles": "Unlock encrypted profiles by entering their password.", "deleteProfileConfirmBtn": "",
"tooltipOpenSettings": "Open the settings pane", "descriptionBlockUnknownConnections": "",
"invalidImportString": "Invalid import string", "descriptionExperiments": "",
"contactAlreadyExists": "Contact Already Exists", "descriptionExperimentsGroups": "",
"conversationSettings": "Conversation Settings", "displayNameLabel": "Nome de Exibição",
"enterCurrentPasswordForDelete": "Please enter current password to delete this profile.", "dmTooltip": "Clique para DM",
"enableGroups": "Enable Group Chat", "dontSavePeerHistory": "",
"experimentsEnabled": "Enable Experiments", "editProfile": "",
"localeIt": "Italiana", "editProfileTitle": "",
"localeEs": "Espanol", "enableGroups": "",
"addListItem": "Adicionar Item à Lista", "enterCurrentPasswordForDelete": "",
"addNewItem": "Adicionar novo item à lista", "enterProfilePassword": "",
"todoPlaceholder": "Afazer…", "error0ProfilesLoadedForPassword": "",
"newConnectionPaneTitle": "New Connection", "experimentsEnabled": "",
"networkStatusOnline": "Online", "groupAddr": "",
"networkStatusConnecting": "Connecting to network and peers...", "groupName": "",
"networkStatusAttemptingTor": "Attempting to connect to Tor network", "groupNameLabel": "Nome do Grupo",
"networkStatusDisconnected": "Disconnected from the internet, check your connection", "invalidImportString": "",
"viewGroupMembershipTooltip": "View Group Membership", "invitation": "",
"loadingTor": "Loading tor...", "invitationLabel": "Convite",
"smallTextLabel": "Pequeno", "inviteBtn": "Convidar",
"defaultScalingText": "Texto tamanho padrão (fator de escala: ", "inviteToGroupLabel": "Convidar ao grupo",
"builddate": "Built on: %2", "joinGroup": "",
"version": "Version %1", "joinGroupTab": "",
"versionTor": "Version %1 with tor %2", "largeTextLabel": "Grande",
"themeDark": "Dark", "listsBtn": "Listas",
"themeLight": "Light", "loadingTor": "",
"settingTheme": "Theme", "localeDe": "",
"largeTextLabel": "Grande", "localeEn": "",
"settingInterfaceZoom": "Zoom level", "localeEs": "",
"localeDe": "Deutsche", "localeFr": "",
"localePt": "Portuguesa", "localeIt": "",
"localeFr": "Frances", "localePt": "",
"localeEn": "English", "membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.",
"settingLanguage": "Language", "networkStatusAttemptingTor": "",
"blockUnknownLabel": "Block Unknown Peers", "networkStatusConnecting": "",
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)", "networkStatusDisconnected": "",
"versionBuilddate": "Version: %1 Built on: %2", "networkStatusOnline": "",
"cwtchSettingsTitle": "Configurações do Cwtch", "newBulletinLabel": "Novo Boletim",
"unlock": "Unlock", "newConnectionPaneTitle": "",
"yourServers": "Your Servers", "newGroupBtn": "Criar novo grupo",
"yourProfiles": "Your Profiles", "newProfile": "",
"error0ProfilesLoadedForPassword": "0 profiles loaded with that password", "noPasswordWarning": "",
"password": "Password", "password": "",
"enterProfilePassword": "Enter a password to view your profiles", "password1Label": "",
"addNewProfileBtn": "Add new profile", "password2Label": "",
"deleteConfirmText": "DELETE", "passwordChangeError": "",
"deleteProfileConfirmBtn": "Really Delete Profile", "passwordErrorEmpty": "",
"deleteConfirmLabel": "Type DELETE to confirm", "passwordErrorMatch": "",
"deleteProfileBtn": "Delete Profile", "pasteAddressToAddContact": "… cole um endereço aqui para adicionar um contato…",
"passwordChangeError": "Error changing password: Supplied password rejected", "peerAddress": "",
"passwordErrorMatch": "Passwords do not match", "peerBlockedMessage": "",
"saveProfileBtn": "Save Profile", "peerName": "",
"createProfileBtn": "Create Profile", "peerNotOnline": "",
"passwordErrorEmpty": "Password cannot be empty", "peerOfflineMessage": "",
"password2Label": "Reenter password", "pendingLabel": "Pendente",
"password1Label": "Password", "postNewBulletinLabel": "Postar novo boletim",
"currentPasswordLabel": "Current Password", "profileName": "",
"yourDisplayName": "Your Display Name", "profileOnionLabel": "",
"profileOnionLabel": "Send this address to peers you want to connect with", "puzzleGameBtn": "Jogo de Adivinhação",
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted", "radioNoPassword": "",
"radioNoPassword": "Unencrypted (No password)", "radioUsePassword": "",
"radioUsePassword": "Password", "rejectGroupBtn": "Recusar",
"copiedToClipboardNotification": "Copiado", "saveBtn": "Salvar",
"copyBtn": "Copiar", "savePeerHistory": "",
"editProfile": "Edit Profille", "savePeerHistoryDescription": "",
"newProfile": "New Profile", "saveProfileBtn": "",
"defaultProfileName": "Alice", "search": "",
"profileName": "Display name", "searchList": "",
"editProfileTitle": "Edit Profile", "server": "",
"addProfileTitle": "Add new profile", "serverConnectivityConnected": "",
"deleteBtn": "Deletar", "serverConnectivityDisconnected": "",
"unblockBtn": "Unblock Peer", "serverInfo": "",
"dontSavePeerHistory": "Delete Peer History", "serverLabel": "Servidor",
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.", "serverNotSynced": "",
"savePeerHistory": "Save Peer History", "serverSynced": "",
"blockBtn": "Block Peer", "settingInterfaceZoom": "",
"saveBtn": "Salvar", "settingLanguage": "",
"displayNameLabel": "Nome de Exibição", "settingTheme": "",
"addressLabel": "Endereço", "smallTextLabel": "Pequeno",
"puzzleGameBtn": "Jogo de Adivinhação", "successfullAddedContact": "",
"bulletinsBtn": "Boletins", "themeDark": "",
"listsBtn": "Listas", "themeLight": "",
"chatBtn": "Chat", "titleManageContacts": "",
"rejectGroupBtn": "Recusar", "titleManageProfiles": "",
"acceptGroupBtn": "Aceitar", "titleManageServers": "",
"acceptGroupInviteLabel": "Você quer aceitar o convite para", "titlePlaceholder": "título…",
"newGroupBtn": "Criar novo grupo", "todoPlaceholder": "Afazer…",
"copiedClipboardNotification": "Copiado", "tooltipAddContact": "",
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now", "tooltipOpenSettings": "",
"peerBlockedMessage": "Peer is blocked", "tooltipUnlockProfiles": "",
"pendingLabel": "Pendente", "unblockBtn": "",
"acknowledgedLabel": "Confirmada", "unlock": "",
"couldNotSendMsgError": "Não deu para enviar esta mensagem", "update": "",
"dmTooltip": "Clique para DM", "version": "",
"membershipDescription": "A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo.", "versionBuilddate": "",
"addListItemBtn": "Add Item", "versionTor": "",
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.", "viewGroupMembershipTooltip": "",
"searchList": "Search List", "viewServerInfo": "",
"update": "Update", "yourDisplayName": "",
"inviteBtn": "Convidar", "yourProfiles": "",
"inviteToGroupLabel": "Convidar ao grupo", "yourServers": "",
"groupNameLabel": "Nome do Grupo", "zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)"
"viewServerInfo": "Server Info",
"serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected",
"serverConnectivityConnected": "Server Connected",
"serverInfo": "Server Information",
"invitationLabel": "Convite",
"serverLabel": "Servidor",
"search": "Search...",
"cycleColoursDesktop": "Click to cycle colours.\nRight-click to reset.",
"cycleColoursAndroid": "Click to cycle colours.\nLong-press to reset.",
"cycleMorphsDesktop": "Click to cycle morphs.\nRight-click to reset.",
"cycleMorphsAndroid": "Click to cycle morphs.\nLong-press to reset.",
"cycleCatsDesktop": "Click to cycle category.\nRight-click to reset.",
"cycleCatsAndroid": "Click to cycle category.\nLong-press to reset.",
"blocked": "Blocked",
"titlePlaceholder": "título…",
"postNewBulletinLabel": "Postar novo boletim",
"newBulletinLabel": "Novo Boletim",
"joinGroup": "Join group",
"createGroup": "Create group",
"addPeer": "Add Peer",
"groupAddr": "Address",
"invitation": "Invitation",
"server": "Server",
"groupName": "Group name",
"peerName": "Name",
"peerAddress": "Address",
"joinGroupTab": "Join a group",
"createGroupTab": "Create a group",
"addPeerTab": "Add a peer",
"createGroupBtn": "Criar",
"defaultGroupName": "Grupo incrível",
"createGroupTitle": "Criar Grupo"
} }

View File

@ -114,7 +114,4 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''');
yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)");
} }

View File

@ -1,7 +1,3 @@
import 'dart:convert';
import 'package:cwtch/notification_manager.dart';
import 'package:cwtch/views/messageview.dart';
import 'package:cwtch/widgets/rightshiftfixer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:cwtch/cwtch/ffi.dart'; import 'package:cwtch/cwtch/ffi.dart';
import 'package:cwtch/cwtch/gomobile.dart'; import 'package:cwtch/cwtch/gomobile.dart';
@ -10,7 +6,6 @@ import 'package:cwtch/errorHandler.dart';
import 'package:cwtch/settings.dart'; import 'package:cwtch/settings.dart';
import 'package:cwtch/torstatus.dart'; import 'package:cwtch/torstatus.dart';
import 'package:cwtch/views/triplecolview.dart'; import 'package:cwtch/views/triplecolview.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart'; import 'cwtch/cwtch.dart';
import 'cwtch/cwtchNotifier.dart'; import 'cwtch/cwtchNotifier.dart';
@ -18,20 +13,16 @@ import 'licenses.dart';
import 'model.dart'; import 'model.dart';
import 'views/profilemgrview.dart'; import 'views/profilemgrview.dart';
import 'views/splashView.dart'; import 'views/splashView.dart';
import 'dart:io' show Platform, exit; import 'dart:io' show Platform;
import 'opaque.dart'; import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var globalSettings = Settings(Locale("en", ''), OpaqueDark()); var globalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalErrorHandler = ErrorHandler(); var globalErrorHandler = ErrorHandler();
var globalTorStatus = TorStatus(); var globalTorStatus = TorStatus();
var globalAppState = AppState();
void main() { void main() {
print("main()");
LicenseRegistry.addLicense(() => licenses()); LicenseRegistry.addLicense(() => licenses());
WidgetsFlutterBinding.ensureInitialized();
print("runApp()");
runApp(Flwtch()); runApp(Flwtch());
} }
@ -44,120 +35,69 @@ class Flwtch extends StatefulWidget {
class FlwtchState extends State<Flwtch> { class FlwtchState extends State<Flwtch> {
final TextStyle biggerFont = const TextStyle(fontSize: 18); final TextStyle biggerFont = const TextStyle(fontSize: 18);
late Cwtch cwtch; Cwtch cwtch;
late ProfileListState profs; bool cwtchInit = false;
final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler'); ProfileInfoState selectedProfile;
final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdown'); String selectedConversation = "";
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>(); var columns = [1]; // default or 'single column' mode
//var columns = [1, 1, 2];
AppModel appStatus;
ProfileListState profs;
@override @override
initState() { initState() {
print("initState: running...");
super.initState(); super.initState();
cwtchInit = false;
print("initState: registering notification, shutdown handlers...");
profs = ProfileListState(); profs = ProfileListState();
notificationClickChannel.setMethodCallHandler(_externalNotificationClicked); var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus);
shutdownMethodChannel.setMethodCallHandler(shutdown);
print("initState: creating cwtchnotifier, ffi");
if (Platform.isAndroid) { if (Platform.isAndroid) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
cwtch = CwtchGomobile(cwtchNotifier); cwtch = CwtchGomobile(cwtchNotifier);
} else if (Platform.isLinux) {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, LinuxNotificationsManager(), globalAppState);
cwtch = CwtchFfi(cwtchNotifier);
} else { } else {
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
cwtch = CwtchFfi(cwtchNotifier); cwtch = CwtchFfi(cwtchNotifier);
} }
print("initState: invoking cwtch.Start()");
cwtch.Start(); cwtch.Start().then((val) {
print("initState: done!"); setState(() {
cwtchInit = true;
});
});
appStatus = AppModel(cwtch: cwtch);
} }
ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus); ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus);
ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler); ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler);
ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings); ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings);
ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this); Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs); ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
globalSettings.initPackageInfo(); //appStatus = AppModel(cwtch: cwtch);
return MultiProvider( return MultiProvider(
providers: [ providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider()],
getFlwtchStateProvider(),
getProfileListProvider(),
getSettingsProvider(),
getErrorHandlerProvider(),
getTorStatusProvider(),
getAppStateProvider(),
],
builder: (context, widget) { builder: (context, widget) {
return Consumer2<Settings, AppState>( Provider.of<Settings>(context).initPackageInfo();
builder: (context, settings, appState, child) => MaterialApp( return Consumer<Settings>(
builder: (context, opaque, child) => MaterialApp(
key: Key('app'), key: Key('app'),
navigatorKey: navKey, locale: Provider.of<Settings>(context).locale,
locale: settings.locale,
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch', title: 'Cwtch',
theme: mkThemeData(settings), theme: mkThemeData(opaque),
home: appState.cwtchInit == true ? ShiftRightFixer(child: ProfileMgrView()) : SplashView(), // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(),
// from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(),
home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ProfileMgrView()) : SplashView(),
), ),
); );
}, },
); );
} }
Future<void> shutdown(MethodCall call) async {
cwtch.Shutdown();
// Wait a few seconds as shutting down things takes a little time..
Future.delayed(Duration(seconds: 2)).then((value) {
if (Platform.isAndroid) {
SystemNavigator.pop();
} else if (Platform.isLinux || Platform.isWindows) {
print("Exiting...");
exit(0);
}
});
}
// Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
Future<void> _externalNotificationClicked(MethodCall call) async {
var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!;
var contact = profile.contactList.getContact(args["RemotePeer"])!;
contact.unreadMessages = 0;
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
var isLandscape = Provider.of<AppState>(navKey.currentContext!, listen: false).isLandscape(navKey.currentContext!);
if (Provider.of<Settings>(navKey.currentContext!, listen: false).uiColumns(isLandscape).length == 1) {
if (navKey.currentContext?.findAncestorWidgetOfExactType<MessageView>() != null) {
print("messageview already open; popping before pushing replacement");
navKey.currentState?.pop();
}
navKey.currentState?.push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: contact),
],
builder: (context, child) => MessageView(),
);
},
),
);
} else { //dual pane
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = args["ProfileOnion"];
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = args["RemotePeer"];
}
}
@override @override
void dispose() { void dispose() {
cwtch.dispose(); cwtch.dispose();

View File

@ -13,8 +13,9 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
var globalSettings = Settings(Locale("en", ''), OpaqueDark()); var globalSettings = Settings(Locale("en", ''), Opaque.dark);
var globalErrorHandler = ErrorHandler(); var globalErrorHandler = ErrorHandler();
void main() { void main() {
@ -62,6 +63,6 @@ class DiskAssetBundle extends CachingAssetBundle {
@override @override
Future<ByteData> load(String key) async { Future<ByteData> load(String key) async {
return _cache[key]!; return _cache[key];
} }
} }

View File

@ -14,11 +14,33 @@ import 'main.dart';
/// UI State /// /// UI State ///
//////////////////// ////////////////////
//todo: delete
class ProfileModel {
String onion;
String nickname;
String creationDate;
String imagePath;
HashMap<String, ContactModel> contacts;
}
//todo: delete
class ContactModel {
String onion;
String nickname;
bool isGroup;
bool isInvitation;
bool isBlocked;
String status;
String imagePath;
ContactModel({this.onion, this.nickname, this.status, this.isInvitation, this.isBlocked, this.imagePath});
}
class ChatMessage { class ChatMessage {
final int o; final int o;
final String d; final String d;
ChatMessage({required this.o, required this.d}); ChatMessage({this.o, this.d});
ChatMessage.fromJson(Map<String, dynamic> json) ChatMessage.fromJson(Map<String, dynamic> json)
: o = json['o'], : o = json['o'],
@ -38,75 +60,39 @@ class ProfileListState extends ChangeNotifier {
List<ProfileInfoState> _profiles = []; List<ProfileInfoState> _profiles = [];
int get num => _profiles.length; int get num => _profiles.length;
void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { void addAll(Iterable<ProfileInfoState> newProfiles) {
var idx = _profiles.indexWhere((element) => element.onion == onion); _profiles.addAll(newProfiles);
if (idx == -1) { notifyListeners();
_profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); }
} else {
_profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); void add(ProfileInfoState newProfile) {
} _profiles.add(newProfile);
notifyListeners(); notifyListeners();
} }
List<ProfileInfoState> get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<ProfileInfoState> get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ProfileInfoState? getProfile(String onion) { ProfileInfoState getProfile(String onion) {
int idx = _profiles.indexWhere((element) => element.onion == onion); int idx = _profiles.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _profiles[idx] : null; return idx >= 0 ? _profiles[idx] : null;
} }
void delete(String onion) {
_profiles.removeWhere((element) => element.onion == onion);
notifyListeners();
}
}
class AppState extends ChangeNotifier {
bool cwtchInit = false;
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
void SetCwtchInit() {
cwtchInit = true;
notifyListeners();
}
void SetAppError(String error) {
appError = error;
notifyListeners();
}
String? get selectedProfile => _selectedProfile;
set selectedProfile(String? newVal) {
this._selectedProfile = newVal;
notifyListeners();
}
String? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) {
this._selectedConversation = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
} }
class ContactListState extends ChangeNotifier { class ContactListState extends ChangeNotifier {
List<ContactInfoState> _contacts = []; List<ContactInfoState> _contacts = [];
String _filter = ""; String _filter;
int get num => _contacts.length; int get num => _contacts.length;
int get numFiltered => isFiltered ? filteredList().length : num; int get numFiltered => isFiltered ? filteredList().length : num;
bool get isFiltered => _filter != ""; bool get isFiltered => _filter != null && _filter != "";
String get filter => _filter; String get filter => _filter;
set filter(String newVal) { set filter(String newVal) {
_filter = newVal.toLowerCase(); _filter = newVal;
notifyListeners(); notifyListeners();
} }
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.contains(_filter) || (c.nickname != null && c.nickname.contains(_filter))).toList();
} }
void addAll(Iterable<ContactInfoState> newContacts) { void addAll(Iterable<ContactInfoState> newContacts) {
@ -154,18 +140,10 @@ class ContactListState extends ChangeNotifier {
List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<ContactInfoState> get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
ContactInfoState? getContact(String onion) { ContactInfoState getContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion); int idx = _contacts.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _contacts[idx] : null; return idx >= 0 ? _contacts[idx] : null;
} }
void removeContact(String onion) {
int idx = _contacts.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_contacts.removeAt(idx);
notifyListeners();
}
}
} }
class ProfileInfoState extends ChangeNotifier { class ProfileInfoState extends ChangeNotifier {
@ -177,25 +155,19 @@ class ProfileInfoState extends ChangeNotifier {
int _unreadMessages = 0; int _unreadMessages = 0;
bool _online = false; bool _online = false;
// assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password.
bool _encrypted = true;
ProfileInfoState({ ProfileInfoState({
required this.onion, this.onion,
nickname = "", nickname = "",
imagePath = "", imagePath = "",
unreadMessages = 0, unreadMessages = 0,
contactsJson = "", contactsJson = "",
serversJson = "", serversJson = "",
online = false, online = false,
encrypted = true,
}) { }) {
this._nickname = nickname; this._nickname = nickname;
this._imagePath = imagePath; this._imagePath = imagePath;
this._unreadMessages = unreadMessages; this._unreadMessages = unreadMessages;
this._online = online; this._online = online;
this._encrypted = encrypted;
if (contactsJson != null && contactsJson != "" && contactsJson != "null") { if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson); List<dynamic> contacts = jsonDecode(contactsJson);
@ -225,23 +197,16 @@ class ProfileInfoState extends ChangeNotifier {
// Parse out the server list json into our server info state struct... // Parse out the server list json into our server info state struct...
void replaceServers(String serversJson) { void replaceServers(String serversJson) {
if (serversJson != "" && serversJson != "null") { if (serversJson != null && serversJson != "" && serversJson != "null") {
print("got servers $serversJson"); print("got servers $serversJson");
List<dynamic> servers = jsonDecode(serversJson); List<dynamic> servers = jsonDecode(serversJson);
this._servers.replace(servers.map((server) { this._servers.replace(servers.map((server) {
// TODO Keys... // TODO Keys...
return ServerInfoState(onion: server["onion"], status: server["status"]); return ServerInfoState(onion: server["onion"], status: server["status"]);
})); }));
notifyListeners();
} }
} }
//
void updateServerStatusCache(String server, String status) {
this._servers.updateServerCache(server, status);
notifyListeners();
}
// Getters and Setters for Online Status // Getters and Setters for Online Status
bool get isOnline => this._online; bool get isOnline => this._online;
set isOnline(bool newValue) { set isOnline(bool newValue) {
@ -249,9 +214,6 @@ class ProfileInfoState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Check encrypted status for profile info screen
bool get isEncrypted => this._encrypted;
String get nickname => this._nickname; String get nickname => this._nickname;
set nickname(String newValue) { set nickname(String newValue) {
this._nickname = newValue; this._nickname = newValue;
@ -286,61 +248,26 @@ class ProfileInfoState extends ChangeNotifier {
super.dispose(); super.dispose();
print("profileinfostate.dispose()"); print("profileinfostate.dispose()");
} }
void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
this._nickname = name;
this._imagePath = picture;
this._online = online;
this.replaceServers(serverJson);
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson);
contacts.forEach((contact) {
var profileContact = this._contacts.getContact(contact["onion"]);
if (profileContact != null) {
profileContact.status = contact["status"];
profileContact.totalMessages = contact["numMessages"];
profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]));
} else {
this._contacts.add(ContactInfoState(
this.onion,
contact["onion"],
nickname: contact["name"],
status: contact["status"],
imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked",
isInvitation: contact["authorization"] == "unknown",
savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"],
numUnread: contact["numUnread"],
isGroup: contact["isGroup"],
server: contact["groupServer"],
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
));
}
});
}
}
} }
class ContactInfoState extends ChangeNotifier { class ContactInfoState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final String onion; final String onion;
late String _nickname; String _nickname;
late bool _isInvitation; bool _isInvitation;
late bool _isBlocked; bool _isBlocked;
late String _status; String _status;
late String _imagePath; String _imagePath;
late String _savePeerHistory; String _savePeerHistory;
late int _unreadMessages = 0; int _unreadMessages = 0;
late int _totalMessages = 0; int _totalMessages = 0;
late DateTime _lastMessageTime; DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageBubbleState>> keys; Map<String, GlobalKey> keys;
// 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; bool _isGroup;
String? _server; String _server;
ContactInfoState( ContactInfoState(
this.profileOnion, this.profileOnion,
@ -355,7 +282,7 @@ class ContactInfoState extends ChangeNotifier {
numMessages = 0, numMessages = 0,
numUnread = 0, numUnread = 0,
lastMessageTime, lastMessageTime,
server, server = "",
}) { }) {
this._nickname = nickname; this._nickname = nickname;
this._isGroup = isGroup; this._isGroup = isGroup;
@ -366,14 +293,14 @@ class ContactInfoState extends ChangeNotifier {
this._totalMessages = numMessages; this._totalMessages = numMessages;
this._unreadMessages = numUnread; this._unreadMessages = numUnread;
this._savePeerHistory = savePeerHistory; this._savePeerHistory = savePeerHistory;
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; this._lastMessageTime = lastMessageTime;
this._server = server; this._server = server;
keys = Map<String, GlobalKey<MessageBubbleState>>(); keys = Map<String, GlobalKey>();
} }
String get nickname => this._nickname; get nickname => this._nickname;
String get savePeerHistory => this._savePeerHistory; get savePeerHistory => this._savePeerHistory;
set savePeerHistory(String newVal) { set savePeerHistory(String newVal) {
this._savePeerHistory = newVal; this._savePeerHistory = newVal;
notifyListeners(); notifyListeners();
@ -384,49 +311,49 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool get isGroup => this._isGroup; get isGroup => this._isGroup;
set isGroup(bool newVal) { set isGroup(bool newVal) {
this._isGroup = newVal; this._isGroup = newVal;
notifyListeners(); notifyListeners();
} }
bool get isBlocked => this._isBlocked; get isBlocked => this._isBlocked;
set isBlocked(bool newVal) { set isBlocked(bool newVal) {
this._isBlocked = newVal; this._isBlocked = newVal;
notifyListeners(); notifyListeners();
} }
bool get isInvitation => this._isInvitation; get isInvitation => this._isInvitation;
set isInvitation(bool newVal) { set isInvitation(bool newVal) {
this._isInvitation = newVal; this._isInvitation = newVal;
notifyListeners(); notifyListeners();
} }
String get status => this._status; get status => this._status;
set status(String newVal) { set status(String newVal) {
this._status = newVal; this._status = newVal;
notifyListeners(); notifyListeners();
} }
int get unreadMessages => this._unreadMessages; get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) { set unreadMessages(int newVal) {
this._unreadMessages = newVal; this._unreadMessages = newVal;
notifyListeners(); notifyListeners();
} }
int get totalMessages => this._totalMessages; get totalMessages => this._totalMessages;
set totalMessages(int newVal) { set totalMessages(int newVal) {
this._totalMessages = newVal; this._totalMessages = newVal;
notifyListeners(); notifyListeners();
} }
String get imagePath => this._imagePath; get imagePath => this._imagePath;
set imagePath(String newVal) { set imagePath(String newVal) {
this._imagePath = newVal; this._imagePath = newVal;
notifyListeners(); notifyListeners();
} }
DateTime get lastMessageTime => this._lastMessageTime; get lastMessageTime => this._lastMessageTime;
set lastMessageTime(DateTime newVal) { set lastMessageTime(DateTime newVal) {
this._lastMessageTime = newVal; this._lastMessageTime = newVal;
notifyListeners(); notifyListeners();
@ -437,8 +364,7 @@ class ContactInfoState extends ChangeNotifier {
bool isOnline() { bool isOnline() {
if (this.isGroup == true) { if (this.isGroup == true) {
// We now have an out of sync warning so we will mark these as online... return this.status == "Synced";
return this.status == "Authenticated" || this.status == "Synced";
} else { } else {
return this.status == "Authenticated"; return this.status == "Authenticated";
} }
@ -448,8 +374,7 @@ class ContactInfoState extends ChangeNotifier {
if (keys[index] == null) { if (keys[index] == null) {
keys[index] = GlobalKey<MessageBubbleState>(); keys[index] = GlobalKey<MessageBubbleState>();
} }
GlobalKey<MessageBubbleState> ret = keys[index]!; return keys[index];
return ret;
} }
} }
@ -457,25 +382,24 @@ class MessageState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final String contactHandle; final String contactHandle;
final int messageIndex; final int messageIndex;
late String _message; String _message;
late int _overlay; int _overlay;
late String _inviteTarget; String _inviteTarget;
late String _inviteNick; String _inviteNick;
late DateTime _timestamp; DateTime _timestamp;
late String _senderOnion; String _senderOnion;
late int _flags; String _senderImage;
String? _senderImage; String _signature = "";
late String _signature = ""; bool _ackd = false;
late bool _ackd = false; bool _error = false;
late bool _error = false; bool _loaded = false;
late bool _loaded = false; bool _malformed = false;
late bool _malformed = false;
MessageState({ MessageState({
required BuildContext context, BuildContext context,
required this.profileOnion, this.profileOnion,
required this.contactHandle, this.contactHandle,
required this.messageIndex, this.messageIndex,
}) { }) {
this._senderOnion = profileOnion; this._senderOnion = profileOnion;
tryLoad(context); tryLoad(context);
@ -484,18 +408,12 @@ class MessageState extends ChangeNotifier {
get message => this._message; get message => this._message;
get overlay => this._overlay; get overlay => this._overlay;
get timestamp => this._timestamp; get timestamp => this._timestamp;
int get flags => this._flags; get ackd => this._ackd;
set flags(int newVal) { get error => this._error;
this._flags = newVal; get malformed => this._malformed;
notifyListeners();
}
bool get ackd => this._ackd;
bool get error => this._error;
bool get malformed => this._malformed;
bool get loaded => this._loaded;
get senderOnion => this._senderOnion; get senderOnion => this._senderOnion;
get senderImage => this._senderImage; get senderImage => this._senderImage;
get loaded => this._loaded;
get signature => this._signature; get signature => this._signature;
get isInvite => this.overlay == 100 || this.overlay == 101; get isInvite => this.overlay == 100 || this.overlay == 101;
get inviteTarget => this._inviteTarget; get inviteTarget => this._inviteTarget;
@ -511,24 +429,13 @@ class MessageState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
set malformed(bool newVal) {
this._malformed = newVal;
notifyListeners();
}
set loaded(bool newVal) {
// quickly-arriving messages get discarded before loading sometimes
if (!hasListeners) return;
this._loaded = newVal;
notifyListeners();
}
void tryLoad(BuildContext context) { void tryLoad(BuildContext context) {
Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) { Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, messageIndex).then((jsonMessage) {
try { try {
dynamic messageWrapper = jsonDecode(jsonMessage); dynamic messageWrapper = jsonDecode(jsonMessage);
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') { if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
this._senderOnion = profileOnion; this._senderOnion = profileOnion;
//todo: remove once sent group messages are prestored
Future.delayed(const Duration(milliseconds: 2), () { Future.delayed(const Duration(milliseconds: 2), () {
tryLoad(context); tryLoad(context);
}); });
@ -537,10 +444,9 @@ class MessageState extends ChangeNotifier {
dynamic message = jsonDecode(messageWrapper['Message']); dynamic message = jsonDecode(messageWrapper['Message']);
this._message = message['d']; this._message = message['d'];
this._overlay = int.parse(message['o'].toString()); this._overlay = int.parse(message['o'].toString());
this._timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!; this._timestamp = DateTime.tryParse(messageWrapper['Timestamp']);
this._senderOnion = messageWrapper['PeerID']; this._senderOnion = messageWrapper['PeerID'];
this._senderImage = messageWrapper['ContactImage']; this._senderImage = messageWrapper['ContactImage'];
this._flags = int.parse(messageWrapper['Flags'].toString(), radix: 2);
// If this is a group, store the signature // If this is a group, store the signature
if (contactHandle.length == 32) { if (contactHandle.length == 32) {
@ -556,6 +462,7 @@ class MessageState extends ChangeNotifier {
} else { } else {
var parts = message['d'].toString().split("||"); var parts = message['d'].toString().split("||");
if (parts.length == 2) { if (parts.length == 2) {
print("jsondecoding: " + utf8.fuse(base64).decode(parts[1].substring(5)));
var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5))); var jsonObj = jsonDecode(utf8.fuse(base64).decode(parts[1].substring(5)));
this._inviteTarget = jsonObj['GroupID']; this._inviteTarget = jsonObj['GroupID'];
this._inviteNick = jsonObj['GroupName']; this._inviteNick = jsonObj['GroupName'];
@ -563,7 +470,7 @@ class MessageState extends ChangeNotifier {
} }
} }
this.loaded = true; this._loaded = true;
//update ackd and error last as they are changenotified //update ackd and error last as they are changenotified
this.ackd = messageWrapper['Acknowledged']; this.ackd = messageWrapper['Acknowledged'];
@ -571,10 +478,42 @@ class MessageState extends ChangeNotifier {
this.error = true; this.error = true;
} }
} catch (e) { } catch (e) {
this._overlay = -1; this._malformed = true;
this.loaded = true;
this.malformed = true;
} }
}); });
} }
} }
/////////////
/// ACN ///
/////////////
class AppModel {
final Cwtch cwtch;
AppModel({this.cwtch});
Stream<String> contactEvents() async* {
while (true) {
String event = await cwtch.ContactEvents();
if (event != "") {
print(event);
yield event;
} else {
print("TEST TEST FAIL TEST FAIL 123");
await Future.delayed(Duration(seconds: 1));
}
}
}
Stream<String> torStatus() async* {
while (true) {
String event = await cwtch.ACNEvents();
if (event != "") {
yield event;
} else {
print("TOR TEST TEST FAIL TEST FAIL 123");
await Future.delayed(Duration(seconds: 1));
}
}
}
}

View File

@ -9,21 +9,11 @@ class ServerListState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
ServerInfoState? getServer(String onion) { ServerInfoState getServer(String onion) {
int idx = _servers.indexWhere((element) => element.onion == onion); int idx = _servers.indexWhere((element) => element.onion == onion);
return idx >= 0 ? _servers[idx] : null; return idx >= 0 ? _servers[idx] : null;
} }
void updateServerCache(String onion, String status) {
int idx = _servers.indexWhere((element) => element.onion == onion);
if (idx >= 0) {
_servers[idx] = ServerInfoState(onion: onion, status: status);
} else {
print("Tried to update server cache without a starting state...this is probably an error");
}
notifyListeners();
}
List<ServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier List<ServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
} }
@ -32,5 +22,5 @@ class ServerInfoState extends ChangeNotifier {
final String onion; final String onion;
final String status; final String status;
ServerInfoState({required this.onion, required this.status}); ServerInfoState({this.onion, this.status});
} }

View File

@ -1,25 +0,0 @@
import 'package:desktop_notifications/desktop_notifications.dart';
import 'package:path/path.dart' as path;
// NotificationsManager provides a wrapper around platform specific notifications logic.
abstract class NotificationsManager {
Future<void> notify(String message);
}
// NullNotificationsManager ignores all notification requests
class NullNotificationsManager implements NotificationsManager {
@override
Future<void> notify(String message) async {}
}
// LinuxNotificationsManager uses the desktop_notifications package to implement
// the standard dbus-powered linux desktop notifications.
class LinuxNotificationsManager implements NotificationsManager {
int previous_id = 0;
final NotificationsClient client = NotificationsClient();
LinuxNotificationsManager() {}
Future<void> notify(String message) async {
var icon_path = Uri.file(path.join(path.current, "cwtch.png"));
client.notify('New Message from Peer!', appName: "cwtch", appIcon: icon_path.toString(), replacesId: this.previous_id).then((Notification value) => previous_id = value.id);
}
}

View File

@ -10,11 +10,6 @@ import 'package:cwtch/settings.dart';
abstract class OpaqueThemeType { abstract class OpaqueThemeType {
static final Color red = Color(0xFFFF0000); static final Color red = Color(0xFFFF0000);
String identifier() {
return "dummy";
}
Color backgroundMainColor() { Color backgroundMainColor() {
return red; return red;
} }
@ -309,15 +304,9 @@ abstract class OpaqueThemeType {
// ... more to come // ... more to come
// Sizes
double contactOnionTextSize() {
return 18;
}
} }
class OpaqueDark extends OpaqueThemeType { class CwtchDark extends OpaqueThemeType {
static final Color darkGreyPurple = Color(0xFF281831); static final Color darkGreyPurple = Color(0xFF281831);
static final Color deepPurple = Color(0xFF422850); static final Color deepPurple = Color(0xFF422850);
static final Color mauvePurple = Color(0xFF8E64A5); static final Color mauvePurple = Color(0xFF8E64A5);
@ -330,10 +319,6 @@ class OpaqueDark extends OpaqueThemeType {
static final Color softGreen = Color(0xFFA0FFB0); static final Color softGreen = Color(0xFFA0FFB0);
static final Color softRed = Color(0xFFFFA0B0); static final Color softRed = Color(0xFFFFA0B0);
String identifier() {
return "dark";
}
Color backgroundMainColor() { Color backgroundMainColor() {
return darkGreyPurple; return darkGreyPurple;
} }
@ -627,7 +612,7 @@ class OpaqueDark extends OpaqueThemeType {
} }
} }
class OpaqueLight extends OpaqueThemeType { class CwtchLight extends OpaqueThemeType {
static final Color whitePurple = Color(0xFFFFFDFF); static final Color whitePurple = Color(0xFFFFFDFF);
static final Color softPurple = Color(0xFFFDF3FC); static final Color softPurple = Color(0xFFFDF3FC);
static final Color purple = Color(0xFFDFB9DE); static final Color purple = Color(0xFFDFB9DE);
@ -640,10 +625,6 @@ class OpaqueLight extends OpaqueThemeType {
static final Color softGreen = Color(0xFFA0FFB0); static final Color softGreen = Color(0xFFA0FFB0);
static final Color softRed = Color(0xFFFFA0B0); static final Color softRed = Color(0xFFFFA0B0);
String identifier() {
return "light";
}
Color backgroundMainColor() { Color backgroundMainColor() {
return whitePurple; return whitePurple;
} }
@ -709,7 +690,7 @@ class OpaqueLight extends OpaqueThemeType {
} }
Color textfieldBackgroundColor() { Color textfieldBackgroundColor() {
return purple; return whitePurple;
} }
Color textfieldBorderColor() { Color textfieldBorderColor() {
@ -741,11 +722,11 @@ class OpaqueLight extends OpaqueThemeType {
} }
Color portraitOnlineBorderColor() { Color portraitOnlineBorderColor() {
return greyPurple; return darkPurple;
} }
Color portraitOnlineBackgroundColor() { Color portraitOnlineBackgroundColor() {
return greyPurple; return darkPurple;
} }
Color portraitOnlineTextColor() { Color portraitOnlineTextColor() {
@ -937,99 +918,6 @@ class OpaqueLight extends OpaqueThemeType {
} }
} }
ThemeData mkThemeData(Settings opaque) {
return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red,
primaryIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
primaryColor: opaque.current().backgroundMainColor(),
canvasColor: opaque.current().backgroundPaneColor(),
backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
cardColor: opaque.current().backgroundMainColor(),
appBarTheme: AppBarTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(
color: opaque.current().mainTextColor(),
),
actionsIconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
)),
bottomNavigationBarTheme: BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed, backgroundColor: opaque.current().backgroundHilightElementColor()),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
)),
),
),
scrollbarTheme: ScrollbarThemeData(
isAlwaysShown: false, thumbColor: MaterialStateProperty.all(opaque.current().scrollbarActiveColor()), trackColor: MaterialStateProperty.all(opaque.current().scrollbarDefaultColor())),
tabBarTheme: TabBarTheme(indicator: UnderlineTabIndicator(borderSide: BorderSide(color: opaque.current().defaultButtonActiveColor()))),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(color: opaque.current().mainTextColor()),
contentTextStyle: TextStyle(color: opaque.current().mainTextColor())),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor()),
headline2: TextStyle(color: opaque.current().mainTextColor()),
headline3: TextStyle(color: opaque.current().mainTextColor()),
headline4: TextStyle(color: opaque.current().mainTextColor()),
headline5: TextStyle(color: opaque.current().mainTextColor()),
headline6: TextStyle(color: opaque.current().mainTextColor()),
bodyText1: TextStyle(color: opaque.current().mainTextColor()),
bodyText2: TextStyle(color: opaque.current().mainTextColor()),
subtitle1: TextStyle(color: opaque.current().mainTextColor()),
subtitle2: TextStyle(color: opaque.current().mainTextColor()),
caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()),
overline: TextStyle(color: opaque.current().mainTextColor())),
switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor()),
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor()),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(backgroundColor: opaque.current().defaultButtonColor(), hoverColor: opaque.current().defaultButtonActiveColor()),
textSelectionTheme: TextSelectionThemeData(
cursorColor: opaque.current().defaultButtonActiveColor(), selectionColor: opaque.current().defaultButtonActiveColor(), selectionHandleColor: opaque.current().defaultButtonActiveColor()),
);
}
/*
OpaqueThemeType _current = CwtchDark();
void setDark() {
_current = CwtchDark();
}
void setLight() {
_current = CwtchLight();
}
OpaqueThemeType current() {
if (_current == null) {
setDark();
}
return _current;
}
class Opaque extends OpaqueThemeType { class Opaque extends OpaqueThemeType {
Color backgroundMainColor() { Color backgroundMainColor() {
return current().backgroundMainColor(); return current().backgroundMainColor();
@ -1337,10 +1225,23 @@ class Opaque extends OpaqueThemeType {
return sidePaneMinSize() + chatPaneMinSize(); return sidePaneMinSize() + chatPaneMinSize();
} }
static late OpaqueThemeType _current; static OpaqueThemeType _current;
//static final OpaqueThemeType dark = CwtchDark(); static final OpaqueThemeType dark = CwtchDark();
//static final OpaqueThemeType light = CwtchLight(); static final OpaqueThemeType light = CwtchLight();
static void setDark() {
_current = dark;
}
static void setLight() {
_current = light;
}
static OpaqueThemeType current() {
if (_current == null) {
setDark();
}
return _current;
}
int scale = 2; int scale = 2;
static final String gcdOS = "linux"; static final String gcdOS = "linux";
@ -1440,4 +1341,51 @@ class Opaque extends OpaqueThemeType {
} }
} }
*/ ThemeData mkThemeData(Settings opaque) {
return ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
primarySwatch: Colors.red,
primaryColor: opaque.current().backgroundMainColor(),
canvasColor: opaque.current().backgroundPaneColor(),
accentColor: opaque.current().defaultButtonColor(),
buttonColor: opaque.current().defaultButtonColor(),
backgroundColor: opaque.current().backgroundMainColor(),
highlightColor: opaque.current().hilightElementTextColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
cardColor: opaque.current().backgroundMainColor(),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle: TextStyle(color: opaque.current().mainTextColor()),
contentTextStyle: TextStyle(color: opaque.current().mainTextColor())),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor()),
headline2: TextStyle(color: opaque.current().mainTextColor()),
headline3: TextStyle(color: opaque.current().mainTextColor()),
headline4: TextStyle(color: opaque.current().mainTextColor()),
headline5: TextStyle(color: opaque.current().mainTextColor()),
headline6: TextStyle(color: opaque.current().mainTextColor()),
bodyText1: TextStyle(color: opaque.current().mainTextColor()),
bodyText2: TextStyle(color: opaque.current().mainTextColor()),
subtitle1: TextStyle(color: opaque.current().mainTextColor()),
subtitle2: TextStyle(color: opaque.current().mainTextColor()),
caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()),
overline: TextStyle(color: opaque.current().mainTextColor())),
);
}

View File

@ -9,37 +9,27 @@ import 'opaque.dart';
const TapirGroupsExperiment = "tapir-groups-experiment"; const TapirGroupsExperiment = "tapir-groups-experiment";
enum DualpaneMode {
Single,
Dual1to2,
Dual1to4,
CopyPortrait,
}
/// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments. /// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments.
/// We also provide access to the version information here as it is also accessed from the /// We also provide access to the version information here as it is also accessed from the
/// Settings Pane. /// Settings Pane.
class Settings extends ChangeNotifier { class Settings extends ChangeNotifier {
Locale locale; Locale locale;
late PackageInfo packageInfo; PackageInfo packageInfo;
OpaqueThemeType theme; OpaqueThemeType theme;
// explicitly set experiments to false until told otherwise... bool experimentsEnabled;
bool experimentsEnabled = false;
HashMap<String, bool> experiments = HashMap.identity(); HashMap<String, bool> experiments = HashMap.identity();
DualpaneMode _uiColumnModePortrait = DualpaneMode.Single;
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
bool blockUnknownConnections = false; bool blockUnknownConnections;
/// Set the dark theme. /// Set the dark theme.
void setDark() { void setDark() {
theme = OpaqueDark(); theme = Opaque.dark;
notifyListeners(); notifyListeners();
} }
/// Set the Light theme. /// Set the Light theme.
void setLight() { void setLight() {
theme = OpaqueLight(); theme = Opaque.light;
notifyListeners(); notifyListeners();
} }
@ -48,18 +38,6 @@ class Settings extends ChangeNotifier {
return theme; return theme;
} }
/// isExperimentEnabled can be used to safely check whether a particular
/// experiment is enabled
bool isExperimentEnabled(String experiment) {
if (this.experimentsEnabled) {
if (this.experiments.containsKey(experiment)) {
// We now know it cannot be null...
return this.experiments[experiment]! == true;
}
}
return false;
}
/// Called by the event bus. When new settings are loaded from a file the JSON will /// Called by the event bus. When new settings are loaded from a file the JSON will
/// be sent to the function and new settings will be instantiated based on the contents. /// be sent to the function and new settings will be instantiated based on the contents.
handleUpdate(dynamic settings) { handleUpdate(dynamic settings) {
@ -82,10 +60,6 @@ class Settings extends ChangeNotifier {
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON // Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
experiments = new HashMap<String, bool>.from(settings["Experiments"]); experiments = new HashMap<String, bool>.from(settings["Experiments"]);
// single pane vs dual pane preferences
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
// Push the experimental settings to Consumers of Settings // Push the experimental settings to Consumers of Settings
notifyListeners(); notifyListeners();
} }
@ -138,62 +112,11 @@ class Settings extends ChangeNotifier {
/// Turn on a specific experiment. /// Turn on a specific experiment.
enableExperiment(String key) { enableExperiment(String key) {
experiments.update(key, (value) => true, ifAbsent: () => true); experiments.update(key, (value) => true, ifAbsent: () => true);
notifyListeners();
} }
/// Turn off a specific experiment /// Turn off a specific experiment
disableExperiment(String key) { disableExperiment(String key) {
experiments.update(key, (value) => false, ifAbsent: () => false); experiments.update(key, (value) => false, ifAbsent: () => false);
notifyListeners();
}
DualpaneMode get uiColumnModePortrait => _uiColumnModePortrait;
set uiColumnModePortrait(DualpaneMode newval) {
this._uiColumnModePortrait = newval;
notifyListeners();
}
DualpaneMode get uiColumnModeLandscape => _uiColumnModeLandscape;
set uiColumnModeLandscape(DualpaneMode newval) {
this._uiColumnModeLandscape = newval;
notifyListeners();
}
List<int> uiColumns(bool isLandscape) {
var m = (!isLandscape || uiColumnModeLandscape == DualpaneMode.CopyPortrait) ? uiColumnModePortrait : uiColumnModeLandscape;
switch(m) {
case DualpaneMode.Single: return [1];
case DualpaneMode.Dual1to2: return [1, 2];
case DualpaneMode.Dual1to4: return [1, 4];
}
print("impossible column configuration: portrait/$uiColumnModePortrait landscape/$uiColumnModeLandscape");
return [1];
}
static List<DualpaneMode> uiColumnModeOptions(bool isLandscape) {
if (isLandscape) return [DualpaneMode.CopyPortrait, DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4,];
else return [DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4];
}
static DualpaneMode uiColumnModeFromString(String m) {
switch(m) {
case "DualpaneMode.Single": return DualpaneMode.Single;
case "DualpaneMode.Dual1to2": return DualpaneMode.Dual1to2;
case "DualpaneMode.Dual1to4": return DualpaneMode.Dual1to4;
case "DualpaneMode.CopyPortrait": return DualpaneMode.CopyPortrait;
}
print("Error: ui requested translation of column mode [$m] which doesn't exist");
return DualpaneMode.Single;
}
static String uiColumnModeToString(DualpaneMode m) {
// todo: translate
switch(m) {
case DualpaneMode.Single: return "Single";
case DualpaneMode.Dual1to2: return "Double (1:2)";
case DualpaneMode.Dual1to4: return "Double (1:4)";
case DualpaneMode.CopyPortrait: return "Same as portrait mode setting";
}
} }
/// Construct a default settings object. /// Construct a default settings object.
@ -202,7 +125,10 @@ class Settings extends ChangeNotifier {
/// Convert this Settings object to a JSON representation for serialization on the /// Convert this Settings object to a JSON representation for serialization on the
/// event bus. /// event bus.
dynamic asJson() { dynamic asJson() {
var themeString = theme.identifier(); var themeString = "light";
if (theme == Opaque.dark) {
themeString = "dark";
}
return { return {
"Locale": this.locale.languageCode, "Locale": this.locale.languageCode,
@ -212,9 +138,7 @@ class Settings extends ChangeNotifier {
"ExperimentsEnabled": this.experimentsEnabled, "ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments, "Experiments": experiments,
"StateRootPane": 0, "StateRootPane": 0,
"FirstTime": false, "FirstTime": false
"UIColumnModePortrait": uiColumnModePortrait.toString(),
"UIColumnModeLandscape": uiColumnModeLandscape.toString(),
}; };
} }
} }

View File

@ -4,9 +4,6 @@ class TorStatus extends ChangeNotifier {
int progress; int progress;
String status; String status;
bool connected; bool connected;
String version;
TorStatus({this.connected = false, this.progress = 0, this.status = "", this.version = ""});
/// Called by the event bus. /// Called by the event bus.
handleUpdate(int new_progress, String new_status) { handleUpdate(int new_progress, String new_status) {
@ -21,9 +18,4 @@ class TorStatus extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
updateVersion(String new_version) {
version = new_version;
notifyListeners();
}
} }

View File

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/errorHandler.dart'; import 'package:cwtch/errorHandler.dart';
@ -27,6 +26,7 @@ class AddContactView extends StatefulWidget {
class _AddContactViewState extends State<AddContactView> { class _AddContactViewState extends State<AddContactView> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _createGroupFormKey = GlobalKey<FormState>(); final _createGroupFormKey = GlobalKey<FormState>();
final _joinGroupFormKey = GlobalKey<FormState>();
final ctrlrOnion = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: "");
final ctrlrContact = TextEditingController(text: ""); final ctrlrContact = TextEditingController(text: "");
final ctrlrGroupName = TextEditingController(text: ""); final ctrlrGroupName = TextEditingController(text: "");
@ -34,14 +34,9 @@ class _AddContactViewState extends State<AddContactView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// if we haven't picked a server yet, pick the first one in the list...
if (server.isEmpty && Provider.of<ProfileInfoState>(context).serverList.servers.isNotEmpty) {
server = Provider.of<ProfileInfoState>(context).serverList.servers.first.onion;
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.titleManageContacts), title: Text(AppLocalizations.of(context).titleManageContacts),
), ),
body: _buildForm(), body: _buildForm(),
); );
@ -50,21 +45,16 @@ class _AddContactViewState extends State<AddContactView> {
Widget _buildForm() { Widget _buildForm() {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion; ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
/// We display a different number of tabs depending on the experiment setup /// We display a different number of tabs dependening on the experiment setup
bool groupsEnabled = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment); bool groupsEnabled = Provider.of<Settings>(context).experimentsEnabled && Provider.of<Settings>(context).experiments[TapirGroupsExperiment];
return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) { return Consumer<ErrorHandler>(builder: (context, globalErrorHandler, child) {
return DefaultTabController( return DefaultTabController(
length: groupsEnabled ? 2 : 1, length: groupsEnabled ? 4 : 1,
child: Column(children: [ child: Column(children: [
(groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()), (groupsEnabled ? getTabBarWithGroups() : getTabBarWithAddPeerOnly()),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
children: (groupsEnabled children: (groupsEnabled ? [addPeerTab(), manageServersTab(), addGroupTab(), joinGroupTab()] : [addPeerTab()]),
? [
addPeerTab(),
addGroupTab(),
]
: [addPeerTab()]),
)), )),
])); ]));
}); });
@ -72,7 +62,7 @@ class _AddContactViewState extends State<AddContactView> {
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)!.copiedClipboardNotification)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
@ -81,8 +71,8 @@ class _AddContactViewState extends State<AddContactView> {
return TabBar( return TabBar(
tabs: [ tabs: [
Tab( Tab(
icon: Icon(CwtchIcons.add_peer), icon: Icon(Icons.person_add_rounded),
text: AppLocalizations.of(context)!.addPeer, text: AppLocalizations.of(context).addPeer,
), ),
], ],
); );
@ -93,11 +83,12 @@ class _AddContactViewState extends State<AddContactView> {
return TabBar( return TabBar(
tabs: [ tabs: [
Tab( Tab(
icon: Icon(CwtchIcons.add_peer), icon: Icon(Icons.person_add_rounded),
text: AppLocalizations.of(context)!.tooltipAddContact, text: AppLocalizations.of(context).addPeer,
), ),
//Tab(icon: Icon(Icons.backup), text: AppLocalizations.of(context)!.titleManageServers), Tab(icon: Icon(Icons.backup), text: AppLocalizations.of(context).titleManageServers),
Tab(icon: Icon(CwtchIcons.add_group), text: AppLocalizations.of(context)!.createGroup), Tab(icon: Icon(Icons.group), text: AppLocalizations.of(context).createGroup),
Tab(icon: Icon(Icons.group_add), text: AppLocalizations.of(context).joinGroup),
], ],
); );
} }
@ -112,24 +103,20 @@ class _AddContactViewState extends State<AddContactView> {
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
key: _formKey, key: _formKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.profileOnionLabel), CwtchLabel(label: AppLocalizations.of(context).profileOnionLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchButtonTextField( CwtchButtonTextField(
controller: ctrlrOnion, controller: ctrlrOnion,
onPressed: _copyOnion, onPressed: _copyOnion,
readonly: true, icon: Icon(Icons.copy),
icon: Icon( tooltip: AppLocalizations.of(context).copyBtn,
CwtchIcons.address_copy_2,
size: 32,
),
tooltip: AppLocalizations.of(context)!.copyBtn,
), ),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.pasteAddressToAddContact), CwtchLabel(label: AppLocalizations.of(context).pasteAddressToAddContact),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -140,25 +127,29 @@ class _AddContactViewState extends State<AddContactView> {
return null; return null;
} }
if (globalErrorHandler.invalidImportStringError) { if (globalErrorHandler.invalidImportStringError) {
return AppLocalizations.of(context)!.invalidImportString; return AppLocalizations.of(context).invalidImportString;
} else if (globalErrorHandler.contactAlreadyExistsError) { } else if (globalErrorHandler.contactAlreadyExistsError) {
return AppLocalizations.of(context)!.contactAlreadyExists; return AppLocalizations.of(context).contactAlreadyExists;
} else if (globalErrorHandler.explicitAddContactSuccess) {} } else if (globalErrorHandler.explicitAddContactSuccess) {}
return null; return null;
}, },
onChanged: (String importBundle) async { onChanged: (String peerAddr) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle); final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": peerAddr},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.importBundleSuccess) { if (globalErrorHandler.explicitAddContactSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + importBundle)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + peerAddr));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context); Navigator.pop(context);
} }
}); });
}, },
labelText: '',
) )
]))); ])));
} }
@ -167,7 +158,12 @@ class _AddContactViewState extends State<AddContactView> {
Widget addGroupTab() { Widget addGroupTab() {
// TODO We should replace with with a "Paste in Server Key Bundle" // TODO We should replace with with a "Paste in Server Key Bundle"
if (Provider.of<ProfileInfoState>(context).serverList.servers.isEmpty) { if (Provider.of<ProfileInfoState>(context).serverList.servers.isEmpty) {
return Text(AppLocalizations.of(context)!.addServerFirst); return Text("You need to add a server before you can create a group.");
}
// if we haven't picked a server yet, pick the first one in the list...
if (server.isEmpty) {
server = Provider.of<ProfileInfoState>(context).serverList.servers.first.onion;
} }
return Container( return Container(
@ -180,59 +176,80 @@ class _AddContactViewState extends State<AddContactView> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CwtchLabel(label: AppLocalizations.of(context)!.server), CwtchLabel(label: AppLocalizations.of(context).server),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
DropdownButton( DropdownButton(
onChanged: (String? newServer) { onChanged: (newServer) {
setState(() { server = newServer;
server = newServer!;
});
}, },
isExpanded: true, // magic property
value: server, value: server,
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) { items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: serverInfo.onion, value: serverInfo.onion,
child: Text( child: Text(serverInfo.onion),
serverInfo.onion,
overflow: TextOverflow.ellipsis,
),
); );
}).toList()), }).toList()),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.groupName), CwtchLabel(label: AppLocalizations.of(context).groupName),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchTextField( CwtchTextField(controller: ctrlrGroupName, labelText: AppLocalizations.of(context).groupNameLabel, onChanged: (newValue) {}),
controller: ctrlrGroupName,
labelText: AppLocalizations.of(context)!.groupNameLabel,
onChanged: (newValue) {},
validator: (value) {},
),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {},
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; child: Text(AppLocalizations.of(context).createGroupBtn),
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateGroup(profileOnion, server, ctrlrGroupName.text);
Future.delayed(const Duration(milliseconds: 500), () {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact + " " + ctrlrGroupName.text));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
});
},
child: Text(AppLocalizations.of(context)!.createGroupBtn),
), ),
], ],
))); )));
} }
/// TODO Join Group Pane
Widget joinGroupTab() {
return Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Form(
autovalidateMode: AutovalidateMode.always,
key: _joinGroupFormKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).joinGroupTab),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrContact,
validator: (value) {
if (value == "") {
return null;
}
if (globalErrorHandler.importBundleError) {
return AppLocalizations.of(context).invalidImportString;
} else if (globalErrorHandler.importBundleSuccess) {}
return null;
},
onChanged: (String importBundle) async {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, importBundle);
Future.delayed(const Duration(milliseconds: 500), () {
if (globalErrorHandler.importBundleSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact + importBundle));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.pop(context);
}
});
},
)
])));
}
/// TODO Manage Servers Tab /// TODO Manage Servers Tab
Widget manageServersTab() { Widget manageServersTab() {
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) { final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -12,14 +11,12 @@ import 'package:cwtch/widgets/textfield.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 '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../main.dart'; import '../main.dart';
import '../opaque.dart'; import '../opaque.dart';
import '../settings.dart'; import '../settings.dart';
class AddEditProfileView extends StatefulWidget { class AddEditProfileView extends StatefulWidget {
const AddEditProfileView({Key? key}) : super(key: key); const AddEditProfileView({Key key}) : super(key: key);
@override @override
_AddEditProfileViewState createState() => _AddEditProfileViewState(); _AddEditProfileViewState createState() => _AddEditProfileViewState();
@ -33,8 +30,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
final ctrlrPass = TextEditingController(text: ""); final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: ""); final ctrlrPass2 = TextEditingController(text: "");
final ctrlrOnion = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: "");
late bool usePassword; bool usePassword;
late bool deleted; bool deleted;
@override @override
void initState() { void initState() {
@ -51,15 +48,15 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion; ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addProfileTitle : AppLocalizations.of(context)!.editProfileTitle), title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addProfileTitle : AppLocalizations.of(context).editProfileTitle),
), ),
body: _buildForm(), body: _buildForm(),
); );
} }
void _handleSwitchPassword(bool? value) { void _handleSwitchPassword(bool value) {
setState(() { setState(() {
usePassword = value!; usePassword = value;
}); });
} }
@ -91,19 +88,16 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
diameter: 120, diameter: 120,
maskOut: false, maskOut: false,
border: theme.theme.portraitOnlineBorderColor(), border: theme.theme.portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
) )
])), ])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchTextField( CwtchTextField(
controller: ctrlrNick, controller: ctrlrNick,
autofocus: false, labelText: AppLocalizations.of(context).yourDisplayName,
labelText: AppLocalizations.of(context)!.yourDisplayName,
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value.isEmpty) {
return "Please enter a display name"; return "Please enter a display name";
@ -118,33 +112,37 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.addressLabel), CwtchLabel(label: AppLocalizations.of(context).addressLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchButtonTextField( CwtchButtonTextField(
controller: ctrlrOnion, controller: ctrlrOnion,
onPressed: _copyOnion, onPressed: _copyOnion,
readonly: true, icon: Icon(Icons.copy),
icon: Icon( tooltip: AppLocalizations.of(context).copyBtn,
CwtchIcons.address_copy_2,
size: 32,
),
tooltip: AppLocalizations.of(context)!.copyBtn,
) )
])), ])),
// We only allow setting password types on profile creation // We only allow setting password types on profile creation
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isEmpty, visible: Provider.of<ProfileInfoState>(context).onion.isEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
Checkbox( Radio(
value: usePassword, value: false,
fillColor: MaterialStateProperty.all(theme.current().defaultButtonColor()), groupValue: usePassword,
activeColor: theme.current().defaultButtonActiveColor(),
onChanged: _handleSwitchPassword, onChanged: _handleSwitchPassword,
), ),
Text( Text(
AppLocalizations.of(context)!.radioUsePassword, AppLocalizations.of(context).radioNoPassword,
style: TextStyle(color: theme.current().mainTextColor()),
),
Radio(
value: true,
groupValue: usePassword,
onChanged: _handleSwitchPassword,
),
Text(
AppLocalizations.of(context).radioUsePassword,
style: TextStyle(color: theme.current().mainTextColor()), style: TextStyle(color: theme.current().mainTextColor()),
), ),
])), ])),
@ -155,9 +153,9 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
visible: usePassword, visible: usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty && Provider.of<ProfileInfoState>(context).isEncrypted, visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel), CwtchLabel(label: AppLocalizations.of(context).currentPasswordLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -165,11 +163,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
controller: ctrlrOldPass, controller: ctrlrOldPass,
validator: (value) { validator: (value) {
// Password field can be empty when just updating the profile, not on creation // Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context).isEncrypted && Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty; return AppLocalizations.of(context).passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deleteProfileError == true) {
return AppLocalizations.of(context)!.enterCurrentPasswordForDelete;
} }
return null; return null;
}, },
@ -178,7 +173,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
height: 20, height: 20,
), ),
])), ])),
CwtchLabel(label: AppLocalizations.of(context)!.newPassword), CwtchLabel(label: AppLocalizations.of(context).password1Label),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -187,10 +182,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
validator: (value) { validator: (value) {
// Password field can be empty when just updating the profile, not on creation // Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty; return AppLocalizations.of(context).passwordErrorEmpty;
} }
if (value != ctrlrPass2.value.text) { if (value != ctrlrPass2.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch; return AppLocalizations.of(context).passwordErrorMatch;
} }
return null; return null;
}, },
@ -198,7 +193,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.password2Label), CwtchLabel(label: AppLocalizations.of(context).password2Label),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -207,10 +202,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
validator: (value) { validator: (value) {
// Password field can be empty when just updating the profile, not on creation // Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty; return AppLocalizations.of(context).passwordErrorEmpty;
} }
if (value != ctrlrPass.value.text) { if (value != ctrlrPass.value.text) {
return AppLocalizations.of(context)!.passwordErrorMatch; return AppLocalizations.of(context).passwordErrorMatch;
} }
return null; return null;
}), }),
@ -219,19 +214,10 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
SizedBox( SizedBox(
height: 20, height: 20,
), ),
Row( ElevatedButton(
mainAxisAlignment: MainAxisAlignment.center, onPressed: _createPressed,
children: [ style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
Expanded( child: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn),
child: ElevatedButton(
onPressed: _createPressed,
child: Text(
Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn,
textAlign: TextAlign.center,
),
),
),
],
), ),
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty, visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
@ -240,14 +226,16 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
height: 20, height: 20,
), ),
Tooltip( Tooltip(
message: AppLocalizations.of(context)!.enterCurrentPasswordForDelete, message: AppLocalizations.of(context).enterCurrentPasswordForDelete,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: checkCurrentPassword()
showAlertDialog(context); ? null
}, : () {
showAlertDialog(context);
},
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()), style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
icon: Icon(Icons.delete_forever), icon: Icon(Icons.delete_forever),
label: Text(AppLocalizations.of(context)!.deleteBtn), label: Text(AppLocalizations.of(context).deleteBtn),
)) ))
])) ]))
])))))); ]))))));
@ -264,7 +252,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
// This will run all the validations in the form including // This will run all the validations in the form including
// checking that display name is not empty, and an actual check that the passwords // checking that display name is not empty, and an actual check that the passwords
// match (and are provided if the user has requested an encrypted profile). // match (and are provided if the user has requested an encrypted profile).
if (_formKey.currentState!.validate()) { if (_formKey.currentState.validate()) {
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) { if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) {
if (usePassword == true) { if (usePassword == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text); Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
@ -310,49 +298,52 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
} }
} }
showAlertDialog(BuildContext context) { // TODO Stub - wire this into a libCwtch call.
// set up the buttons bool checkCurrentPassword() {
Widget cancelButton = TextButton( return ctrlrOldPass.value.text.isEmpty;
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
onPressed: () {
var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteProfile(onion, ctrlrOldPass.value.text);
Future.delayed(
const Duration(milliseconds: 500),
() {
if (globalErrorHandler.deleteProfileSuccess) {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteProfileSuccess + ":" + onion));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.of(context).popUntil((route) => route.isFirst); // dismiss dialog
} else {
Navigator.of(context).pop();
}
},
);
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
} }
} }
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()),
overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()),
padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context).deleteProfileConfirmBtn),
onPressed: () {
// TODO Actually Delete the Peer
Navigator.of(context).pop(); // dismiss dialog
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context).deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

View File

@ -1,4 +1,3 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/contactrow.dart';
@ -13,14 +12,14 @@ import '../model.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ContactsView extends StatefulWidget { class ContactsView extends StatefulWidget {
const ContactsView({Key? key}) : super(key: key); const ContactsView({Key key}) : super(key: key);
@override @override
_ContactsViewState createState() => _ContactsViewState(); _ContactsViewState createState() => _ContactsViewState();
} }
class _ContactsViewState extends State<ContactsView> { class _ContactsViewState extends State<ContactsView> {
late TextEditingController ctrlrFilter; TextEditingController ctrlrFilter;
bool showSearchBar = false; bool showSearchBar = false;
@override @override
@ -32,66 +31,67 @@ class _ContactsViewState extends State<ContactsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
endDrawerEnableOpenDragGesture: false, appBar: AppBar(
drawerEnableOpenDragGesture: false, title: Row(children: [
appBar: AppBar( ProfileImage(
title: RepaintBoundary( imagePath: Provider.of<ProfileInfoState>(context).imagePath,
child: Row(children: [ diameter: 42,
ProfileImage( border: Provider.of<Settings>(context).theme.portraitOnlineBorderColor(),
imagePath: Provider.of<ProfileInfoState>(context).imagePath, ),
diameter: 42, SizedBox(
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(), width: 10,
badgeTextColor: Colors.red, ),
badgeColor: Colors.red, Expanded(
), child: Text(
SizedBox( "%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname ?? Provider.of<ProfileInfoState>(context).onion ?? '').replaceAll("%2", "Contacts"),
width: 10, overflow: TextOverflow.ellipsis,
), )), //todo
Expanded( ]),
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts), actions: [
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor()))), //todo IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
])), IconButton(
actions: [ icon: Icon(Icons.copy),
IconButton(icon: TorIcon(), onPressed: _pushTorStatus), onPressed: _copyOnion,
IconButton( ),
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset IconButton(
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search), // need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
onPressed: () { icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
Provider.of<ContactListState>(context, listen: false).filter = ""; onPressed: () {
setState(() { Provider.of<ContactListState>(context, listen: false).filter = "";
showSearchBar = !showSearchBar; setState(() {
}); showSearchBar = !showSearchBar;
}) });
], })
), ],
floatingActionButton: FloatingActionButton( ),
onPressed: _pushAddContact, floatingActionButton: FloatingActionButton(
tooltip: AppLocalizations.of(context)!.tooltipAddContact, onPressed: _pushAddContact,
child: const Icon(CwtchIcons.person_add_alt_1_24px), tooltip: AppLocalizations.of(context).tooltipAddContact,
), child: const Icon(Icons.person_add_sharp),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList()); ),
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList(),
);
} }
Widget _buildFilterable() { Widget _buildFilterable() {
Widget txtfield = CwtchTextField( Widget txtfield = CwtchTextField(
controller: ctrlrFilter, controller: ctrlrFilter,
labelText: AppLocalizations.of(context)!.search, labelText: AppLocalizations.of(context).search,
onChanged: (newVal) { onChanged: (newVal) {
Provider.of<ContactListState>(context, listen: false).filter = newVal; Provider.of<ContactListState>(context, listen: false).filter = newVal;
}, });
);
return Column(children: [Padding(padding: EdgeInsets.all(8), child: txtfield), Expanded(child: _buildContactList())]); return Column(children: [Padding(padding: EdgeInsets.all(8), child: txtfield), Expanded(child: _buildContactList())]);
} }
Widget _buildContactList() { Widget _buildContactList() {
final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) { final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.profileOnion + "" + contact.onion), value: contact, builder: (_, __) => RepaintBoundary(child: ContactRow())); return ChangeNotifierProvider<ContactInfoState>.value(key: ValueKey(contact.profileOnion+""+contact.onion), value: contact, builder: (_, __) => ContactRow());
}); });
final divided = ListTile.divideTiles( final divided = ListTile.divideTiles(
context: context, context: context,
tiles: tiles, tiles: tiles,
).toList(); ).toList();
return RepaintBoundary(child: ListView(children: divided)); return ListView(children: divided);
} }
void _pushAddContact() { void _pushAddContact() {
@ -117,4 +117,10 @@ class _ContactsViewState extends State<ContactsView> {
}, },
)); ));
} }
void _copyOnion() {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification)); //todo
// Find the Scaffold in the widget tree and use it to show a SnackBar.
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
} }

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart';
import 'contactsview.dart'; import 'contactsview.dart';
import 'messageview.dart'; import 'messageview.dart';
@ -15,25 +14,22 @@ class DoubleColumnView extends StatefulWidget {
class _DoubleColumnViewState extends State<DoubleColumnView> { class _DoubleColumnViewState extends State<DoubleColumnView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var flwtch = Provider.of<AppState>(context); var flwtch = Provider.of<FlwtchState>(context);
var cols = Provider.of<Settings>(context).uiColumns(true);
return Flex( return Flex(
direction: Axis.horizontal, direction: Axis.horizontal,
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
flex: cols[0], flex: flwtch.columns[0],
child: ContactsView( child: ContactsView(),
key: widget.key,
),
), ),
Flexible( Flexible(
flex: cols[1], flex: flwtch.columns[1],
child: flwtch.selectedConversation == null child: flwtch.selectedConversation == ""
? Card(child:Center(child: Text(AppLocalizations.of(context)!.addContactFirst))) ? Center(child: Text("pick a contact"))
: //dev : //dev
MultiProvider(providers: [ MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)), ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)!), ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation)),
], child: Container(child: MessageView())), ], child: Container(child: MessageView())),
), ),
], ],

View File

@ -1,6 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/opaque.dart'; import 'package:cwtch/opaque.dart';
@ -9,7 +7,6 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart'; import '../main.dart';
import '../config.dart';
/// Global Settings View provides access to modify all the Globally Relevant Settings including Locale, Theme and Experiments. /// Global Settings View provides access to modify all the Globally Relevant Settings including Locale, Theme and Experiments.
class GlobalSettingsView extends StatefulWidget { class GlobalSettingsView extends StatefulWidget {
@ -27,7 +24,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.cwtchSettingsTitle), title: Text(AppLocalizations.of(context).cwtchSettingsTitle),
), ),
body: _buildSettingsList(), body: _buildSettingsList(),
); );
@ -46,13 +43,13 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
), ),
child: Column(children: [ child: Column(children: [
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.settingLanguage, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).settingLanguage, style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(CwtchIcons.change_language, color: settings.current().mainTextColor()), leading: Icon(Icons.language, color: settings.current().mainTextColor()),
trailing: DropdownButton( trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode, value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String? newValue) { onChanged: (String newValue) {
setState(() { setState(() {
settings.switchLocale(Locale(newValue!, '')); settings.switchLocale(Locale(newValue, ''));
saveSettings(context); saveSettings(context);
}); });
}, },
@ -63,8 +60,8 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
); );
}).toList())), }).toList())),
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.settingTheme, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).settingTheme, style: TextStyle(color: settings.current().mainTextColor())),
value: settings.current().identifier() == "light", value: settings.current() == Opaque.light,
onChanged: (bool value) { onChanged: (bool value) {
if (value) { if (value) {
settings.setLight(); settings.setLight();
@ -75,43 +72,31 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings... // Save Settings...
saveSettings(context); saveSettings(context);
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor(), secondary: Icon(Icons.lightbulb_outline, color: settings.current().mainTextColor()),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor()),
), ),
ListTile( ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Portrait Mode", style: TextStyle(color: settings.current().mainTextColor())), title: Text(/*AppLocalizations.of(context).settingLanguage*/ "UI Columns", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()), leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton( trailing: DropdownButton(
value: settings.uiColumnModePortrait.toString(), value: "Single",
onChanged: (String? newValue) { onChanged: (String newValue) {
settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!); if (newValue == "Double (1:2)") {
saveSettings(context); Provider.of<FlwtchState>(context).columns = [1, 2];
} else if (newValue == "Double (1:4)") {
Provider.of<FlwtchState>(context).columns = [1, 4];
} else {
Provider.of<FlwtchState>(context).columns = [1];
}
}, },
items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) { items: ["Single", "Double (1:2)", "Double (1:4)"].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: value.toString(), value: value,
child: Text(Settings.uiColumnModeToString(value)), child: Text(value),
);
}).toList())),
ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Landscape Mode", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value)),
); );
}).toList())), }).toList())),
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionBlockUnknownConnections), subtitle: Text(AppLocalizations.of(context).descriptionBlockUnknownConnections),
value: settings.blockUnknownConnections, value: settings.blockUnknownConnections,
onChanged: (bool value) { onChanged: (bool value) {
if (value) { if (value) {
@ -123,13 +108,11 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings... // Save Settings...
saveSettings(context); saveSettings(context);
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor(), secondary: Icon(Icons.app_blocking, color: settings.current().mainTextColor()),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor()),
), ),
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperiments), subtitle: Text(AppLocalizations.of(context).descriptionExperiments),
value: settings.experimentsEnabled, value: settings.experimentsEnabled,
onChanged: (bool value) { onChanged: (bool value) {
if (value) { if (value) {
@ -140,18 +123,16 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings... // Save Settings...
saveSettings(context); saveSettings(context);
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor(), secondary: Icon(Icons.science, color: settings.current().mainTextColor()),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_experiments, color: settings.current().mainTextColor()),
), ),
Visibility( Visibility(
visible: settings.experimentsEnabled, visible: settings.experimentsEnabled,
child: Column( child: Column(
children: [ children: [
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.enableGroups, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).enableGroups, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.descriptionExperimentsGroups), subtitle: Text(AppLocalizations.of(context).descriptionExperimentsGroups),
value: settings.isExperimentEnabled(TapirGroupsExperiment), value: settings.experiments.containsKey(TapirGroupsExperiment) && settings.experiments[TapirGroupsExperiment],
onChanged: (bool value) { onChanged: (bool value) {
if (value) { if (value) {
settings.enableExperiment(TapirGroupsExperiment); settings.enableExperiment(TapirGroupsExperiment);
@ -161,17 +142,21 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
// Save Settings... // Save Settings...
saveSettings(context); saveSettings(context);
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor(), secondary: Icon(Icons.group_sharp, color: settings.current().mainTextColor()),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
), ),
], ],
)), )),
AboutListTile( AboutListTile(
icon: Icon(Icons.info, color: settings.current().mainTextColor()), icon: Icon(Icons.info, color: settings.current().mainTextColor()),
applicationIcon: Padding(padding:EdgeInsets.all(5), child: Icon(CwtchIcons.cwtch_knott)), applicationIcon: Padding(
padding: EdgeInsets.all(20),
child: Image(
image: AssetImage("assets/knott.png"),
width: 128,
height: 128,
)),
applicationName: "Cwtch (Flutter UI)", applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context)!.versionBuilddate.replaceAll("%1", EnvironmentConfig.BUILD_VER).replaceAll("%2", EnvironmentConfig.BUILD_DATE), applicationVersion: AppLocalizations.of(context).version.replaceAll("%1", constructVersionString(Provider.of<Settings>(context).packageInfo)),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society', applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
), ),
])))); ]))));
@ -192,22 +177,22 @@ String constructVersionString(PackageInfo pinfo) {
/// an individual language code. There might be a more efficient way of doing this. /// an individual language code. There might be a more efficient way of doing this.
String getLanguageFull(context, String languageCode) { String getLanguageFull(context, String languageCode) {
if (languageCode == "en") { if (languageCode == "en") {
return AppLocalizations.of(context)!.localeEn; return AppLocalizations.of(context).localeEn;
} }
if (languageCode == "es") { if (languageCode == "es") {
return AppLocalizations.of(context)!.localeEs; return AppLocalizations.of(context).localeEs;
} }
if (languageCode == "fr") { if (languageCode == "fr") {
return AppLocalizations.of(context)!.localeFr; return AppLocalizations.of(context).localeFr;
} }
if (languageCode == "pt") { if (languageCode == "pt") {
return AppLocalizations.of(context)!.localePt; return AppLocalizations.of(context).localePt;
} }
if (languageCode == "de") { if (languageCode == "de") {
return AppLocalizations.of(context)!.localeDe; return AppLocalizations.of(context).localeDe;
} }
if (languageCode == "it") { if (languageCode == "it") {
return AppLocalizations.of(context)!.localeIt; return AppLocalizations.of(context).localeIt;
} }
return languageCode; return languageCode;
} }

View File

@ -1,4 +1,3 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/model.dart'; import 'package:cwtch/model.dart';
import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/buttontextfield.dart';
@ -43,7 +42,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(Provider.of<ContactInfoState>(context).nickname + " " + AppLocalizations.of(context)!.conversationSettings), title: Text(Provider.of<ContactInfoState>(context).nickname + " " + AppLocalizations.of(context).conversationSettings),
), ),
body: _buildSettingsList(), body: _buildSettingsList(),
); );
@ -61,15 +60,40 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Container( child: Container(
margin: EdgeInsets.all(10), margin: EdgeInsets.all(30),
padding: EdgeInsets.all(2), padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).groupAddr),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrGroupAddr,
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).server),
SizedBox(
height: 20,
),
CwtchTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).server),
)
]),
// Nickname Save Button // Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -81,70 +105,21 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
var handle = Provider.of<ContactInfoState>(context, listen: false).onion; var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text; Provider.of<ContactInfoState>(context, listen: false).nickname = ctrlrNick.text;
Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text); Provider.of<FlwtchState>(context, listen: false).cwtch.SetGroupAttribute(profileOnion, handle, "local.name", ctrlrNick.text);
// todo translations
final snackBar = SnackBar(content: Text("Group Nickname changed successfully"));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, },
icon: Icon(Icons.save), icon: Icon(Icons.save),
tooltip: AppLocalizations.of(context)!.saveBtn, tooltip: AppLocalizations.of(context).saveBtn,
)
]),
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.groupAddr),
SizedBox(
height: 20,
),
CwtchTextField(
controller: ctrlrGroupAddr,
labelText: '',
validator: (value) {},
) )
]), ]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.server), CwtchLabel(label: AppLocalizations.of(context).conversationSettings),
SizedBox(
height: 20,
),
CwtchTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).server),
validator: (value) {},
labelText: '',
)
]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.conversationSettings),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
// TODO // TODO
]), ]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(CwtchIcons.leave_group),
label: Text(AppLocalizations.of(context)!.leaveGroup),
))
])
]))))); ])))));
}); });
}); });
@ -152,47 +127,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
void _copyOnion() { void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.LeaveGroup(profileOnion, handle);
Future.delayed(Duration(milliseconds: 500), () {
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.reallyLeaveThisGroupPrompt),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
} }

View File

@ -1,9 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart'; import 'package:cwtch/views/peersettingsview.dart';
import 'package:cwtch/widgets/DropdownContacts.dart'; import 'package:cwtch/widgets/DropdownContacts.dart';
@ -43,32 +39,17 @@ class _MessageViewState extends State<MessageView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return WillPopScope( return WillPopScope(
onWillPop: _onWillPop, onWillPop: _onWillPop,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
// setting leading to null makes it do the default behaviour; container() hides it title: Text(Provider.of<ContactInfoState>(context).nickname),
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
title: Row(children: [
ProfileImage(
imagePath: Provider.of<ContactInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
SizedBox(
width: 10,
),Text(Provider.of<ContactInfoState>(context).nickname)]),
actions: [ actions: [
//IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings), IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings),
//IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings), IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings),
//IconButton(icon: Icon(Icons.push_pin), onPressed: _pushContactSettings), IconButton(icon: Icon(Icons.push_pin), onPressed: _pushContactSettings),
IconButton( IconButton(icon: Icon(Icons.settings), onPressed: _pushContactSettings),
icon: Provider.of<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px), IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _debugResetContact),
tooltip: AppLocalizations.of(context)!.conversationSettings,
onPressed: _pushContactSettings),
], ],
), ),
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList()), body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList()),
@ -81,6 +62,12 @@ class _MessageViewState extends State<MessageView> {
return true; return true;
} }
void _debugResetContact() {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.DebugResetContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
}
void _pushContactSettings() { void _pushContactSettings() {
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext bcontext) { builder: (BuildContext bcontext) {
@ -99,7 +86,7 @@ class _MessageViewState extends State<MessageView> {
)); ));
} }
void _sendMessage([String? ignoredParam]) { void _sendMessage([String ignoredParam]) {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
@ -107,7 +94,7 @@ class _MessageViewState extends State<MessageView> {
_sendMessageHelper(); _sendMessageHelper();
} }
void _sendInvitation([String? ignoredParam]) { void _sendInvitation([String ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact); .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, this.selectedContact);
@ -127,39 +114,37 @@ class _MessageViewState extends State<MessageView> {
Widget _buildComposeBox() { Widget _buildComposeBox() {
return Container( return Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor(), color: Provider.of<Settings>(context).theme.backgroundMainColor(),
padding: EdgeInsets.all(2),
margin: EdgeInsets.all(2),
height: 100, height: 100,
padding: EdgeInsets.all(8.0),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Container( child: TextField(
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))), key: Key('txtCompose'),
child: TextFormField( controller: ctrlrCompose,
key: Key('txtCompose'), focusNode: focusNode,
controller: ctrlrCompose, textInputAction: TextInputAction.send,
autofocus: !Platform.isAndroid, onSubmitted: _sendMessage,
focusNode: focusNode, )),
textInputAction: TextInputAction.send, Column(children: [
onFieldSubmitted: _sendMessage, SizedBox(
decoration: InputDecoration( width: 100,
enabledBorder: InputBorder.none, height: 50,
focusedBorder: InputBorder.none, child: Padding(
enabled: true, padding: EdgeInsets.fromLTRB(2, 2, 2, 2),
prefixIcon: IconButton( child: ElevatedButton(
icon: Icon(CwtchIcons.send_invite, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()), child: Icon(Icons.send, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendInvite, style: ButtonStyle(
enableFeedback: true, fixedSize: MaterialStateProperty.all(Size(86, 50)),
splashColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(), backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.defaultButtonColor()),
hoverColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
onPressed: () => _modalSendInvitation(context)),
suffixIcon: IconButton(
icon: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
tooltip: AppLocalizations.of(context)!.sendMessage,
onPressed: _sendMessage,
), ),
onPressed: _sendMessage,
))), ))),
), SizedBox(
width: 86,
height: 40,
child: IconButton(icon: Icon(Icons.insert_invitation, size: 12, color: Provider.of<Settings>(context).theme.mainTextColor()), onPressed: () => _modalSendInvitation(context))),
])
], ],
), ),
); );
@ -183,15 +168,13 @@ class _MessageViewState extends State<MessageView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text(AppLocalizations.of(bcontext)!.invitationLabel), Text(AppLocalizations.of(bcontext).invitationLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: Provider.of<ProfileInfoState>(ctx, listen: false), value: Provider.of<ProfileInfoState>(ctx, listen: false),
child: DropdownContacts(filter: (contact) { child: DropdownContacts(onChanged: (newVal) {
return contact.onion != Provider.of<ContactInfoState>(context).onion;
}, onChanged: (newVal) {
setState(() { setState(() {
this.selectedContact = newVal; this.selectedContact = newVal;
}); });
@ -200,11 +183,9 @@ class _MessageViewState extends State<MessageView> {
height: 20, height: 20,
), ),
ElevatedButton( ElevatedButton(
child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn), child: Text(AppLocalizations.of(bcontext).inviteBtn, semanticsLabel: AppLocalizations.of(bcontext).inviteBtn),
onPressed: () { onPressed: () {
if (this.selectedContact != "") { this._sendInvitation();
this._sendInvitation();
}
Navigator.pop(bcontext); Navigator.pop(bcontext);
}, },
), ),

View File

@ -1,5 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/model.dart'; import 'package:cwtch/model.dart';
import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/buttontextfield.dart';
@ -56,11 +55,31 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: Container( child: Container(
margin: EdgeInsets.all(10), margin: EdgeInsets.all(30),
padding: EdgeInsets.all(2), padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).addressLabel),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context).copyBtn,
)
]),
// Nickname Save Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
@ -77,40 +96,21 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
}; };
final setPeerAttributeJson = jsonEncode(setPeerAttribute); final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.nickChangeSuccess));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, },
icon: Icon(Icons.save), icon: Icon(Icons.save),
tooltip: AppLocalizations.of(context)!.saveBtn, tooltip: AppLocalizations.of(context).saveBtn,
)
]),
// Address Copy Button
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
CwtchLabel(label: AppLocalizations.of(context)!.addressLabel),
SizedBox(
height: 20,
),
CwtchButtonTextField(
controller: TextEditingController(text: Provider.of<ContactInfoState>(context, listen: false).onion),
onPressed: _copyOnion,
icon: Icon(Icons.copy),
tooltip: AppLocalizations.of(context)!.copyBtn,
) )
]), ]),
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox( SizedBox(
height: 20, height: 20,
), ),
CwtchLabel(label: AppLocalizations.of(context)!.conversationSettings), CwtchLabel(label: AppLocalizations.of(context).conversationSettings),
SizedBox( SizedBox(
height: 20, height: 20,
), ),
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context)!.blockBtn, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).blockBtn, style: TextStyle(color: settings.current().mainTextColor())),
value: Provider.of<ContactInfoState>(context).isBlocked, value: Provider.of<ContactInfoState>(context).isBlocked,
onChanged: (bool blocked) { onChanged: (bool blocked) {
// Save local blocked status // Save local blocked status
@ -138,20 +138,18 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
} }
}, },
activeTrackColor: settings.theme.defaultButtonActiveColor(), secondary: Icon(Icons.block, color: settings.current().mainTextColor()),
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(CwtchIcons.block_peer, color: settings.current().mainTextColor()),
), ),
ListTile( ListTile(
title: Text(AppLocalizations.of(context)!.savePeerHistory, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).savePeerHistory, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context)!.savePeerHistoryDescription), subtitle: Text(AppLocalizations.of(context).savePeerHistoryDescription),
leading: Icon(CwtchIcons.peer_history, color: settings.current().mainTextColor()), leading: Icon(Icons.history_sharp, color: settings.current().mainTextColor()),
trailing: DropdownButton( trailing: DropdownButton(
value: Provider.of<ContactInfoState>(context).savePeerHistory == "DefaultDeleteHistory" value: Provider.of<ContactInfoState>(context).savePeerHistory == "DefaultDeleteHistory"
? AppLocalizations.of(context)!.dontSavePeerHistory ? AppLocalizations.of(context).dontSavePeerHistory
: (Provider.of<ContactInfoState>(context).savePeerHistory == "SaveHistory" : (Provider.of<ContactInfoState>(context).savePeerHistory == "SaveHistory"
? AppLocalizations.of(context)!.savePeerHistory ? AppLocalizations.of(context).savePeerHistory
: AppLocalizations.of(context)!.dontSavePeerHistory), : AppLocalizations.of(context).dontSavePeerHistory),
onChanged: (newValue) { onChanged: (newValue) {
setState(() { setState(() {
// Set whether or not to dave the Contact History... // Set whether or not to dave the Contact History...
@ -159,7 +157,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
var onion = Provider.of<ContactInfoState>(context, listen: false).onion; var onion = Provider.of<ContactInfoState>(context, listen: false).onion;
const SaveHistoryKey = "SavePeerHistory"; const SaveHistoryKey = "SavePeerHistory";
if (newValue == AppLocalizations.of(context)!.savePeerHistory) { if (newValue == AppLocalizations.of(context).savePeerHistory) {
Provider.of<ContactInfoState>(context, listen: false).savePeerHistory = "SaveHistory"; Provider.of<ContactInfoState>(context, listen: false).savePeerHistory = "SaveHistory";
final setPeerAttribute = { final setPeerAttribute = {
"EventType": "SetPeerAttribute", "EventType": "SetPeerAttribute",
@ -179,29 +177,13 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
} }
}); });
}, },
items: [AppLocalizations.of(context)!.savePeerHistory, AppLocalizations.of(context)!.dontSavePeerHistory].map<DropdownMenuItem<String>>((String value) { items: [AppLocalizations.of(context).savePeerHistory, AppLocalizations.of(context).dontSavePeerHistory].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: value, value: value,
child: Text(value), child: Text(value),
); );
}).toList())), }).toList())),
]), ]),
Column(mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [
SizedBox(
height: 20,
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
child: ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
icon: Icon(CwtchIcons.leave_chat),
label: Text(AppLocalizations.of(context)!.leaveGroup),
))
])
]),
]))))); ])))));
}); });
}); });
@ -209,47 +191,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
void _copyOnion() { void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion)); Clipboard.setData(new ClipboardData(text: Provider.of<ContactInfoState>(context, listen: false).onion));
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.copiedClipboardNotification)); final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text("Cancel"),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.LeaveConversation(profileOnion, handle);
Future.delayed(Duration(milliseconds: 500), () {
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
});
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.reallyLeaveThisGroupPrompt),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
} }

View File

@ -1,20 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/settings.dart'; import 'package:cwtch/settings.dart';
import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/views/torstatusview.dart';
import 'package:cwtch/widgets/passwordfield.dart'; import 'package:cwtch/widgets/passwordfield.dart';
import 'package:cwtch/widgets/tor_icon.dart'; import 'package:cwtch/widgets/tor_icon.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:cwtch/widgets/profilerow.dart'; import 'package:cwtch/widgets/profilerow.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../config.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../torstatus.dart';
import 'addeditprofileview.dart'; import 'addeditprofileview.dart';
import 'globalsettingsview.dart'; import 'globalsettingsview.dart';
@ -28,8 +23,6 @@ class ProfileMgrView extends StatefulWidget {
class _ProfileMgrViewState extends State<ProfileMgrView> { class _ProfileMgrViewState extends State<ProfileMgrView> {
final ctrlrPassword = TextEditingController(); final ctrlrPassword = TextEditingController();
bool closeApp = false;
@override @override
void dispose() { void dispose() {
ctrlrPassword.dispose(); ctrlrPassword.dispose();
@ -38,115 +31,45 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<Settings>( // Prevents Android back button from closing the app on the profile manager screen
// Prevents Android back button from closing the app on the profile manager screen // (which would shutdown connections and all kinds of other expensive to generate things)
// (which would shutdown connections and all kinds of other expensive to generate things) // TODO pop up a dialogue regarding closing the app?
// TODO pop up a dialogue regarding closing the app? return new WillPopScope(
builder: (context, settings, child) => onWillPop: () async => false,
WillPopScope( child: Scaffold(
onWillPop: () async { backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor(),
_showShutdown(); appBar: AppBar(
return closeApp; title: Text(AppLocalizations.of(context).titleManageProfiles),
}, actions: [
child: Scaffold( IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
backgroundColor: settings.theme.backgroundMainColor(), IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _setLoggingLevelDebug),
appBar: AppBar( IconButton(
title: Row(children: [ icon: Icon(Icons.lock_open),
Image( tooltip: AppLocalizations.of(context).tooltipUnlockProfiles,
image: AssetImage("assets/core/knott-white.png"), onPressed: _modalUnlockProfiles,
filterQuality: FilterQuality.medium, ),
isAntiAlias: true, IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context).tooltipOpenSettings, onPressed: _pushGlobalSettings),
width: 32, ],
height: 32, ),
colorBlendMode: BlendMode.dstIn, floatingActionButton: FloatingActionButton(
color: Provider onPressed: _pushAddEditProfile,
.of<Settings>(context) tooltip: AppLocalizations.of(context).addNewProfileBtn,
.theme child: Icon(
.backgroundHilightElementColor(), Icons.add,
), semanticLabel: AppLocalizations.of(context).addNewProfileBtn,
SizedBox( ),
width: 10, ),
), body: _buildProfileManager(), //_buildSuggestions(),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor()))) ));
]),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
body: _buildProfileManager(),
)),
);
} }
List<Widget> getActions() { void _setLoggingLevelDebug() {
List<Widget> actions = new List<Widget>.empty(growable: true); final setLoggingLevel = {
"EventType": "SetLoggingLevel",
// Tor Status "Data": {"Debug": "true"},
actions.add(IconButton( };
icon: TorIcon(), final setLoggingLevelJson = jsonEncode(setLoggingLevel);
onPressed: _pushTorStatus, Provider.of<FlwtchState>(context, listen: false).cwtch.SendAppEvent(setLoggingLevelJson);
tooltip: Provider.of<TorStatus>(context).progress == 100
? AppLocalizations.of(context)!.networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),
));
// Only show debug button on development builds
// Unlock Profiles
actions.add(IconButton(
icon: Icon(CwtchIcons.lock_open_24px),
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
onPressed: _modalUnlockProfiles,
));
// Global Settings
actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings));
actions.add(IconButton(icon: Icon(Icons.close), tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip, onPressed: _showShutdown));
return actions;
}
_showShutdown() {
// set up the buttons
Widget cancelButton = TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
child: Text(AppLocalizations.of(context)!.shutdownCwtchAction),
onPressed: () {
// Directly call the shutdown command, Android will do this for us...
Provider.of<FlwtchState>(context, listen: false).shutdown(MethodCall(""));
closeApp = true;
});
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.shutdownCwtchDialogTitle),
content: Text(AppLocalizations.of(context)!.shutdownCwtchDialog),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return alert;
},
);
} }
void _pushGlobalSettings() { void _pushGlobalSettings() {
@ -189,83 +112,55 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
void _modalUnlockProfiles() { void _modalUnlockProfiles() {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
isScrollControlled: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return Padding( return Container(
padding: MediaQuery.of(context).viewInsets, height: 200, // bespoke value courtesy of the [TextField] docs
child: RepaintBoundary( child: Center(
child: Container( child: Padding(
height: 200, // bespoke value courtesy of the [TextField] docs padding: EdgeInsets.all(10.0),
child: Center( child: Column(
child: Padding( mainAxisAlignment: MainAxisAlignment.center,
padding: EdgeInsets.all(10.0), mainAxisSize: MainAxisSize.min,
child: Column( children: <Widget>[
mainAxisAlignment: MainAxisAlignment.center, Text(AppLocalizations.of(context).enterProfilePassword),
mainAxisSize: MainAxisSize.min, SizedBox(
children: <Widget>[ height: 20,
Text(AppLocalizations.of(context)!.enterProfilePassword), ),
SizedBox( CwtchPasswordField(
height: 20, controller: ctrlrPassword,
), ),
CwtchPasswordField( SizedBox(
autofocus: true, height: 20,
controller: ctrlrPassword, ),
action: unlock, ElevatedButton(
validator: (value) {}, child: Text(AppLocalizations.of(context).unlock, semanticsLabel: AppLocalizations.of(context).unlock),
), onPressed: () {
SizedBox( Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text);
height: 20, ctrlrPassword.text = "";
), Navigator.pop(context);
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ },
Spacer(), ),
Expanded( ],
child: ElevatedButton( )),
child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock), ));
onPressed: () {
unlock(ctrlrPassword.value.text);
},
)),
Spacer()
]),
],
))),
)));
}); });
} }
void unlock(String password) {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(password);
ctrlrPassword.text = "";
Navigator.pop(context);
}
Widget _buildProfileManager() { Widget _buildProfileManager() {
return Consumer<ProfileListState>( final tiles = Provider.of<ProfileListState>(context).profiles.map(
builder: (context, pls, child) { (ProfileInfoState profile) {
final tiles = pls.profiles.map( return ChangeNotifierProvider<ProfileInfoState>.value(
(ProfileInfoState profile) { value: profile,
return ChangeNotifierProvider<ProfileInfoState>.value( builder: (context, child) => ProfileRow(),
value: profile,
builder: (context, child) => RepaintBoundary(child: ProfileRow()),
);
},
); );
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
if (tiles.isEmpty) {
return const Center(
child: const Text(
"Please create or unlock a profile to begin!",
textAlign: TextAlign.center,
));
}
return ListView(children: divided);
}, },
); );
final divided = ListTile.divideTiles(
context: context,
tiles: tiles,
).toList();
return ListView(children: divided);
} }
} }

View File

@ -1,37 +1,13 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import '../settings.dart';
class SplashView extends StatelessWidget { class SplashView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AppState>( print("SplashView build()");
builder: (context, appState, child) => Scaffold( return Scaffold(
body: Center( appBar: AppBar(title: Text("Cwtch")),
child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ body: Center(child: Column(children: <Widget>[Text("Loading Cwtch...")])),
Image( );
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 200,
height: 200,
),
Image(
image: AssetImage("assets/cwtch_title.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text(appState.appError == "" ? "Loading Cwtch..." : appState.appError,
style: TextStyle(
fontSize: 16.0, color: appState.appError == "" ? Provider.of<Settings>(context).theme.mainTextColor() : Provider.of<Settings>(context).theme.textfieldErrorColor())),
),
Image(image: AssetImage("assets/Open_Privacy_Logo_lightoutline.png")),
])),
));
} }
} }

View File

@ -23,7 +23,7 @@ class _TorStatusView extends State<TorStatusView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.torNetworkStatus), title: Text("Tor Network Status"),
), ),
body: _buildSettingsList(), body: _buildSettingsList(),
); );
@ -43,19 +43,15 @@ class _TorStatusView extends State<TorStatusView> {
child: Column(children: [ child: Column(children: [
ListTile( ListTile(
leading: TorIcon(), leading: TorIcon(),
title: Text(AppLocalizations.of(context)!.torStatus), title: Text("Tor Status"),
subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context)!.networkStatusOnline : torStatus.status), subtitle: Text(torStatus.progress == 100 ? AppLocalizations.of(context).networkStatusOnline : torStatus.status),
trailing: ElevatedButton( trailing: ElevatedButton(
child: Text(AppLocalizations.of(context)!.resetTor), child: Text("Reset"),
onPressed: () { onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.ResetTor(); Provider.of<FlwtchState>(context, listen: false).cwtch.ResetTor();
}, },
), ),
), )
ListTile(
title: Text(AppLocalizations.of(context)!.torVersion),
subtitle: Text(torStatus.version),
),
])))); ]))));
}); });
}); });

View File

@ -1,14 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/views/profilemgrview.dart'; import 'package:cwtch/views/profilemgrview.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'contactsview.dart'; import 'contactsview.dart';
import 'messageview.dart'; import 'messageview.dart';
// currently unused but maybe one day?
class TripleColumnView extends StatefulWidget { class TripleColumnView extends StatefulWidget {
@override @override
_TripleColumnViewState createState() => _TripleColumnViewState(); _TripleColumnViewState createState() => _TripleColumnViewState();
@ -17,23 +14,20 @@ class TripleColumnView extends StatefulWidget {
class _TripleColumnViewState extends State<TripleColumnView> { class _TripleColumnViewState extends State<TripleColumnView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context); var flwtch = Provider.of<FlwtchState>(context);
var settings = Provider.of<Settings>(context);
var columns = settings.uiColumns(appState.isLandscape(context));
return Flex(direction: Axis.horizontal, children: <Widget>[ return Flex(direction: Axis.horizontal, children: <Widget>[
Flexible( Flexible(
flex: columns[0], flex: flwtch.columns[0],
child: ProfileMgrView(), child: ProfileMgrView(),
), ),
Flexible( Flexible(
flex: columns[1], flex: flwtch.columns[1],
child: appState.selectedProfile == null ? Center(child: Text(AppLocalizations.of(context)!.createProfileToBegin)) : ContactsView(), //dev child: flwtch.selectedProfile == null ? Center(child: Text("pick a profile")) : ContactsView(), //dev
), ),
Flexible( Flexible(
flex: columns[2], flex: flwtch.columns[2],
child: appState.selectedConversation == null child: flwtch.selectedConversation == ""
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst)) ? Center(child: Text("pick a contact"))
: //dev : //dev
Container(child: MessageView()), Container(child: MessageView()),
), ),

View File

@ -3,41 +3,37 @@ import 'package:provider/provider.dart';
import '../model.dart'; import '../model.dart';
bool noFilter(ContactInfoState peer) {
return true;
}
// Dropdown menu populated from Provider.of<ProfileInfoState>'s contact list // Dropdown menu populated from Provider.of<ProfileInfoState>'s contact list
// Includes both peers and groups; begins empty/nothing selected // Includes both peers and groups; begins empty/nothing selected
// Displays nicknames to UI but uses handles as values // Displays nicknames to UI but uses handles as values
// Pass an onChanged handler to access value // Pass an onChanged handler to access value
class DropdownContacts extends StatefulWidget { class DropdownContacts extends StatefulWidget {
DropdownContacts({ DropdownContacts({
required this.onChanged, this.onChanged,
this.filter = noFilter,
}); });
final Function(dynamic) onChanged; final Function(dynamic) onChanged;
final bool Function(ContactInfoState) filter;
@override @override
_DropdownContactsState createState() => _DropdownContactsState(); _DropdownContactsState createState() => _DropdownContactsState();
} }
class _DropdownContactsState extends State<DropdownContacts> { class _DropdownContactsState extends State<DropdownContacts> {
String? selected; String selected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButton( return DropdownButton(
value: this.selected, value: this.selected,
items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.where(widget.filter).map<DropdownMenuItem<String>>((ContactInfoState contact) { items: Provider.of<ProfileInfoState>(context, listen: false).contactList.contacts.map<DropdownMenuItem<String>>((ContactInfoState contact) {
return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname)); return DropdownMenuItem<String>(value: contact.onion, child: Text(contact.nickname ?? contact.onion));
}).toList(), }).toList(),
onChanged: (String? newVal) { onChanged: (newVal) {
setState(() { setState(() {
this.selected = newVal; this.selected = newVal;
}); });
widget.onChanged(newVal); if (widget.onChanged != null) {
widget.onChanged(newVal);
}
}); });
} }
} }

View File

@ -5,9 +5,9 @@ import 'package:provider/provider.dart';
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchButtonTextField extends StatefulWidget { class CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true}); CwtchButtonTextField({this.controller, this.onPressed, this.icon, this.tooltip, this.readonly = true});
final TextEditingController controller; final TextEditingController controller;
final Function()? onPressed; final Function onPressed;
final Icon icon; final Icon icon;
final String tooltip; final String tooltip;
final bool readonly; final bool readonly;
@ -17,18 +17,6 @@ class CwtchButtonTextField extends StatefulWidget {
} }
class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> { class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
late final FocusNode _focusNode;
@override
void initState() {
_focusNode = FocusNode();
_focusNode.addListener(() {
// Select all...
if (_focusNode.hasFocus) widget.controller.selection = TextSelection(baseOffset: 0, extentOffset: widget.controller.text.length);
});
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<Settings>(builder: (context, theme, child) { return Consumer<Settings>(builder: (context, theme, child) {
@ -36,12 +24,10 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
controller: widget.controller, controller: widget.controller,
readOnly: widget.readonly, readOnly: widget.readonly,
showCursor: !widget.readonly, showCursor: !widget.readonly,
focusNode: _focusNode,
decoration: InputDecoration( decoration: InputDecoration(
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: widget.onPressed, onPressed: widget.onPressed,
icon: widget.icon, icon: widget.icon,
padding: EdgeInsets.fromLTRB(0.0, 4.0, 2.0, 2.0),
tooltip: widget.tooltip, tooltip: widget.tooltip,
enableFeedback: true, enableFeedback: true,
color: theme.current().mainTextColor(), color: theme.current().mainTextColor(),
@ -51,7 +37,6 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
), ),
floatingLabelBehavior: FloatingLabelBehavior.never, floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true, filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)), focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)), errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
@ -59,8 +44,10 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
color: theme.current().textfieldErrorColor(), color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
); );
}); });
} }

View File

@ -20,8 +20,6 @@ class _ContactRowState extends State<ContactRow> {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
return Card( return Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
color: Provider.of<AppState>(context).selectedConversation == contact.onion ? Provider.of<Settings>(context).theme.backgroundHilightElementColor() : null,
borderOnForeground: false,
child: InkWell( child: InkWell(
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding( Padding(
@ -33,7 +31,7 @@ class _ContactRowState extends State<ContactRow> {
diameter: 64.0, diameter: 64.0,
imagePath: contact.imagePath, imagePath: contact.imagePath,
maskOut: !contact.isOnline(), maskOut: !contact.isOnline(),
border: contact.isOnline() ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()), border: contact.isOnline() ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
), ),
Expanded( Expanded(
child: Padding( child: Padding(
@ -43,19 +41,16 @@ class _ContactRowState extends State<ContactRow> {
children: [ children: [
Text( Text(
contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),// contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),//
style: Provider.of<FlwtchState>(context).biggerFont,
style: TextStyle(fontSize: Provider.of<Settings>(context).theme.contactOnionTextSize(),
color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor()), //Provider.of<FlwtchState>(context).biggerFont,
softWrap: true, softWrap: true,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
), ),
Text(contact.onion, Text(contact.onion),
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
], ],
))), ))),
Padding( Padding(
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: contact.isInvitation == true child: contact.isInvitation != null && contact.isInvitation
? Wrap(direction: Axis.vertical, children: <Widget>[ ? Wrap(direction: Axis.vertical, children: <Widget>[
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -82,32 +77,28 @@ class _ContactRowState extends State<ContactRow> {
]), ]),
onTap: () { onTap: () {
setState(() { setState(() {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts var flwtch = Provider.of<FlwtchState>(context, listen: false);
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0; flwtch.setState(() => flwtch.selectedConversation = contact.onion);
// triggers update in Double/TripleColumnView // case 2/3 handled by Double/TripleColumnView respectively
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion; if (flwtch.columns.length == 1) _pushMessageView(contact.onion);
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);
}); });
}, },
)); ));
} }
void _pushMessageView(String handle) { void _pushMessageView(String handle) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle).unreadMessages = 0;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
builder: (BuildContext builderContext) { builder: (BuildContext builderContext) {
// assert we have an actual profile... var profile = Provider.of<FlwtchState>(builderContext, listen: false).profs.getProfile(profileOnion);
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!), ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)),
], ],
builder: (context, child) => MessageView(), builder:(context, child) => MessageView(),
); );
}, },
), ),
@ -132,13 +123,8 @@ class _ContactRowState extends State<ContactRow> {
String dateToNiceString(DateTime date) { String dateToNiceString(DateTime date) {
if (date.millisecondsSinceEpoch == 0) { if (date.millisecondsSinceEpoch == 0) {
return AppLocalizations.of(context)!.dateNever; return AppLocalizations.of(context).dateNever;
} }
// If the last message was over a day ago, just state the date return DateFormat.yMd().add_jm().format(date.toLocal());
if (DateTime.now().difference(date).inDays > 1) {
return DateFormat.yMd().format(date.toLocal());
}
// Otherwise just state the time.
return DateFormat.Hm().format(date.toLocal());
} }
} }

View File

@ -4,8 +4,9 @@ import '../settings.dart';
// Provides a styled Label // Provides a styled Label
// Callers must provide a label text // Callers must provide a label text
// TODO: Integrate this with a settings "zoom" / accessibility setting
class CwtchLabel extends StatefulWidget { class CwtchLabel extends StatefulWidget {
CwtchLabel({required this.label}); CwtchLabel({this.label});
final String label; final String label;
@override @override

View File

@ -1,19 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart';
// Like MessageBubble but for displaying chat overlay 100/101 invitations // Like MessageBubble but for displaying chat overlay 100/101 invitations
// Offers the user an accept/reject button if they don't have a matching contact already // Offers the user an accept/reject button if they don't have a matching contact already
// todo: Reject buttons currently aren't tracked and will reset when the message is reloaded
class InvitationBubble extends StatefulWidget { class InvitationBubble extends StatefulWidget {
@override @override
InvitationBubbleState createState() => InvitationBubbleState(); InvitationBubbleState createState() => InvitationBubbleState();
@ -21,21 +18,15 @@ class InvitationBubble extends StatefulWidget {
class InvitationBubbleState extends State<InvitationBubble> { class InvitationBubbleState extends State<InvitationBubble> {
bool rejected = false; bool rejected = false;
bool isAccepted = false; FocusNode _focus = FocusNode();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (Provider.of<MessageState>(context).malformed) {
return MalformedBubble();
}
var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageState>(context).senderOnion == Provider.of<ProfileInfoState>(context).onion;
var isGroup = Provider.of<MessageState>(context).overlay == 101; var isGroup = Provider.of<MessageState>(context).overlay == 101;
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null; var isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null;
var prettyDate = ""; var prettyDate = "";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageState>(context).flags & 0x01 == 0x01;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
if (Provider.of<MessageState>(context).timestamp != null) { if (Provider.of<MessageState>(context).timestamp != null) {
@ -43,52 +34,70 @@ class InvitationBubbleState extends State<InvitationBubble> {
prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp); prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
} }
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) { if (Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion); var contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact != null) { if (contact == null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion; senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
} else {
senderDisplayStr = contact.nickname ?? contact.onion;
} }
} }
var wdgSender = Center( var wdgSender = Center(
widthFactor: 1, widthFactor: 1,
child: SelectableText(senderDisplayStr + '\u202F', child: SelectableText(senderDisplayStr + '\u202F',
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())));
// If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from // todo: translations
// some kind of malfeasance. var messageStr = "";
var selfInvite = Provider.of<MessageState>(context).inviteNick == Provider.of<ProfileInfoState>(context).onion; if (fromMe) {
if (selfInvite) { //todo: get group name?
return MalformedBubble(); messageStr = "You sent an invitation for " + (isGroup ? "a group" : Provider.of<MessageState>(context).message ?? "");
} else {
messageStr = (isGroup ? "You have been invited to join " + (Provider.of<MessageState>(context).inviteNick ?? "") : "This is a contact suggestion for:") +
"\n" +
(Provider.of<MessageState>(context).inviteTarget ?? "");
} }
var wdgMessage = Center(
var wdgMessage = isGroup && !showGroupInvite ? widthFactor: 1,
Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) : child: SelectableText(
fromMe messageStr + '\u202F',
? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation, key: Key(myKey),
isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget)!.nickname : Provider.of<MessageState>(context).message, myKey) focusNode: _focus,
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of<MessageState>(context).inviteNick, style: TextStyle(
Provider.of<MessageState>(context).inviteTarget, myKey)); color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
));
Widget wdgDecorations; Widget wdgDecorations;
if (isGroup && !showGroupInvite) { if (fromMe) {
wdgDecorations = Text('\u202F'); wdgDecorations = Center(
} else if (fromMe) { widthFactor: 1.0,
wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate); child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(prettyDate,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
!fromMe
? SizedBox(width: 1, height: 1)
: Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
],
));
} else if (isAccepted) { } else if (isAccepted) {
wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F'); wdgDecorations = Text("Accepted!");
} else if (this.rejected) { } else if (this.rejected) {
wdgDecorations = Text(AppLocalizations.of(context)!.rejected + '\u202F'); wdgDecorations = Text("Rejected.");
} else { } else {
wdgDecorations = Center( wdgDecorations = Center(
widthFactor: 1, widthFactor: 1,
child: Wrap(children: [ child: Row(children: [
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F'), onPressed: _btnReject)), Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Reject"), onPressed: _btnReject)),
Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)), Padding(padding: EdgeInsets.all(5), child: TextButton(child: Text("Accept"), onPressed: _btnAccept)),
])); ]));
} }
@ -112,83 +121,35 @@ class InvitationBubbleState extends State<InvitationBubble> {
widthFactor: 1.0, widthFactor: 1.0,
child: Padding( child: Padding(
padding: EdgeInsets.all(9.0), padding: EdgeInsets.all(9.0),
child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(isGroup && !showGroupInvite ? CwtchIcons.enable_experiments : CwtchIcons.send_invite, size: 32))), Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(4), child: Icon(Icons.group_add, size: 32))),
Center( Center(
widthFactor: 1.0, widthFactor: 1.0,
child: Column( child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]), children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])),
)
]))))); ])))));
}); });
} }
void _btnReject() { void _btnReject() {
setState(() { //todo: how should we track inline invite rejections?
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; setState(() => this.rejected = true);
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
var idx = Provider.of<MessageState>(context, listen: false).messageIndex;
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageState>(context, listen: false).flags | 0x01);
Provider.of<MessageState>(context).flags |= 0x01;
});
} }
void _btnAccept() { void _btnAccept() {
setState(() { var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; if (Provider.of<MessageState>(context, listen: false).overlay == 100) {
final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": Provider.of<MessageState>(context, listen: false).message},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of<MessageState>(context, listen: false).message); Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, Provider.of<MessageState>(context, listen: false).message);
isAccepted = true; }
});
}
// Construct an invite chrome for the sender
Widget senderInviteChrome(String chrome, String targetName, String myKey) {
return Wrap(children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
)
]);
}
// Construct an invite chrome
Widget inviteChrome(String chrome, String targetName, String targetId, String myKey) {
return Wrap(children: [
SelectableText(
chrome + '\u202F',
style: TextStyle(
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
maxLines: 2,
),
SelectableText(
targetName + '\u202F',
key: Key(myKey),
style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: TextAlign.left,
maxLines: 2,
textWidthBasis: TextWidthBasis.longestLine,
)
]);
} }
} }

View File

@ -1,6 +1,10 @@
import 'package:cwtch/cwtch_icons_icons.dart'; import 'dart:convert';
import 'dart:ffi';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart';
import '../settings.dart';
final Color malformedColor = Color(0xFFE85DA1); final Color malformedColor = Color(0xFFE85DA1);
@ -36,17 +40,22 @@ class MalformedBubbleState extends State<MalformedBubble> {
widthFactor: 1, widthFactor: 1,
child: Padding( child: Padding(
padding: EdgeInsets.all(4), padding: EdgeInsets.all(4),
child: Icon( child: Image(
CwtchIcons.favorite_black_24dp_broken, image: AssetImage("assets/core/broken_heart_24.png"),
size: 24, filterQuality: FilterQuality.medium,
))), // We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: BlendMode.srcIn,
color: Provider.of<Settings>(context).theme.mainTextColor(),
isAntiAlias: false,
width: 32,
height: 32))),
Center( Center(
widthFactor: 1.0, widthFactor: 1.0,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [Text(AppLocalizations.of(context)!.malformedMessage)], children: [Text("Malformed Message")],
)) ))
]))))); ])))));
}); });

View File

@ -1,11 +1,9 @@
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../model.dart'; import '../model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart';
class MessageBubble extends StatefulWidget { class MessageBubble extends StatefulWidget {
@override @override
@ -24,18 +22,16 @@ class MessageBubbleState extends State<MessageBubble> {
if (Provider.of<MessageState>(context).timestamp != null) { if (Provider.of<MessageState>(context).timestamp != null) {
// user-configurable timestamps prolly ideal? #todo // user-configurable timestamps prolly ideal? #todo
DateTime messageDate = Provider.of<MessageState>(context).timestamp; prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageState>(context).timestamp);
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
} }
// If the sender is not us, then we want to give them a nickname...
var senderDisplayStr = ""; var senderDisplayStr = "";
if (!fromMe && Provider.of<MessageState>(context).senderOnion != null) { if (Provider.of<MessageState>(context).senderOnion != null) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion); var contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).senderOnion);
if (contact != null) { if (contact == null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageState>(context).senderOnion; senderDisplayStr = Provider.of<MessageState>(context).senderOnion;
} else {
senderDisplayStr = contact.nickname ?? contact.onion;
} }
} }
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
@ -52,38 +48,49 @@ class MessageBubbleState extends State<MessageBubble> {
textWidthBasis: TextWidthBasis.longestLine, textWidthBasis: TextWidthBasis.longestLine,
); );
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate); var wdgDecorations = Center(
widthFactor: 1.0,
var error = Provider.of<MessageState>(context).error; child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(prettyDate,
style: TextStyle(
fontSize: 9.0,
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: fromMe ? TextAlign.right : TextAlign.left),
!fromMe
? SizedBox(width: 1, height: 1)
: Provider.of<MessageState>(context).ackd == true
? Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: (Provider.of<MessageState>(context).error == true
? Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12)
: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 12))
],
));
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
//print(constraints.toString()+", "+constraints.maxWidth.toString()); //print(constraints.toString()+", "+constraints.maxWidth.toString());
return RepaintBoundary( return Container(
child: Container( child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
color: error border:
? malformedColor Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()), borderRadius: BorderRadius.only(
border: Border.all( topLeft: Radius.circular(borderRadiousEh),
color: error topRight: Radius.circular(borderRadiousEh),
? malformedColor bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
: (fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()), bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
width: 1), ),
borderRadius: BorderRadius.only( ),
topLeft: Radius.circular(borderRadiousEh), child: Padding(
topRight: Radius.circular(borderRadiousEh), padding: EdgeInsets.all(9.0),
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero, child: Column(
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh), crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
), mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
), mainAxisSize: MainAxisSize.min,
child: Padding( children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))));
padding: EdgeInsets.all(9.0),
child: Column(
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
}); });
} }
} }

View File

@ -1,50 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Provides message decorations (acks/errors/dates etc.) for generic message bubble overlays (chats, invites etc.)
class MessageBubbleDecoration extends StatefulWidget {
MessageBubbleDecoration({required this.ackd, required this.errored, required this.prettyDate, required this.fromMe});
final String prettyDate;
final bool fromMe;
final bool ackd;
final bool errored;
@override
_MessageBubbleDecoration createState() => _MessageBubbleDecoration();
}
class _MessageBubbleDecoration extends State<MessageBubbleDecoration> {
@override
Widget build(BuildContext context) {
return Center(
widthFactor: 1.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.prettyDate,
style:
TextStyle(fontSize: 9.0, color: widget.fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
textAlign: widget.fromMe ? TextAlign.right : TextAlign.left),
!widget.fromMe
? SizedBox(width: 1, height: 1)
: Padding(
padding: EdgeInsets.all(1.0),
child: widget.ackd == true
? Tooltip(
message: AppLocalizations.of(context)!.acknowledgedLabel,
child: Icon(Icons.check_circle_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
: (widget.errored == true
? Tooltip(
message: AppLocalizations.of(context)!.couldNotSendMsgError,
child: Icon(Icons.error_outline, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))
: Tooltip(
message: AppLocalizations.of(context)!.pendingLabel,
child: Icon(Icons.hourglass_bottom_outlined, color: Provider.of<Settings>(context).theme.messageFromMeTextColor(), size: 16))))
],
));
;
}
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../model.dart'; import '../model.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagerow.dart'; import 'messagerow.dart';
@ -16,69 +15,41 @@ class _MessageListState extends State<MessageList> {
@override @override
Widget build(BuildContext outerContext) { Widget build(BuildContext outerContext) {
bool showEphemeralWarning = (Provider.of<ContactInfoState>(context).isGroup == false && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory"); return Card(
bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false; child: Scrollbar(
bool showMessageWarning = showEphemeralWarning || showOfflineWarning; isAlwaysShown: true,
bool showSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status != "Synced"; controller: ctrlr1,
child: Container(
return RepaintBoundary( // Only show broken heart is the contact is offline...
child: Container( decoration: BoxDecoration(
child: Column(children: [ image: Provider.of<ContactInfoState>(outerContext).isOnline()
Visibility( ? null
visible: showMessageWarning, : DecorationImage(
child: Container( fit: BoxFit.contain,
padding: EdgeInsets.all(5.0), image: AssetImage("assets/core/negative_heart_512px.png"),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(), colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.mainTextColor(), BlendMode.srcIn))),
child: showSyncing ? child: ListView.builder(
Text(AppLocalizations.of(context)!.serverNotSynced, controller: ctrlr1,
textAlign: TextAlign.center) itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
: showOfflineWarning reverse: true,
? Text(Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage, itemBuilder: (itemBuilderContext, index) {
textAlign: TextAlign.center) var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
// Only show the ephemeral status for peer conversations, not for groups... return ChangeNotifierProvider(
: (showEphemeralWarning key: ValueKey(trueIndex),
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center) create: (x) => MessageState(
: context: itemBuilderContext,
// We are not allowed to put null here, so put an empty text widge profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
Text("")), contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
)), messageIndex: trueIndex,
Expanded( ),
child: Scrollbar( builder: (bcontext, child) {
controller: ctrlr1, String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
child: Container( ? Provider.of<MessageState>(bcontext).signature
// Only show broken heart is the contact is offline... : trueIndex.toString();
decoration: BoxDecoration( return MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
image: Provider.of<ContactInfoState>(outerContext).isOnline() });
? null },
: DecorationImage( ),
fit: BoxFit.scaleDown, )));
alignment: Alignment.center,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementTextColor(), BlendMode.srcIn))),
// Don't load messages for syncing server...
child: ListView.builder(
controller: ctrlr1,
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...
itemBuilder: (itemBuilderContext, index) {
var trueIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
return ChangeNotifierProvider(
key: ValueKey(trueIndex),
create: (x) => MessageState(
context: itemBuilderContext,
profileOnion: Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
// We don't want to listen for updates to the contact handle...
contactHandle: Provider.of<ContactInfoState>(x, listen: false).onion,
messageIndex: trueIndex,
),
builder: (bcontext, child) {
String idx = Provider.of<ContactInfoState>(outerContext).isGroup == true && Provider.of<MessageState>(bcontext).signature.isEmpty == false
? Provider.of<MessageState>(bcontext).signature
: trueIndex.toString();
return RepaintBoundary(child: MessageRow(key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx)));
});
},
))))
])));
} }
} }

View File

@ -14,7 +14,7 @@ import 'messagebubble.dart';
import 'messageloadingbubble.dart'; import 'messageloadingbubble.dart';
class MessageRow extends StatefulWidget { class MessageRow extends StatefulWidget {
MessageRow({Key? key}) : super(key: key); MessageRow({Key key}) : super(key: key);
@override @override
_MessageRowState createState() => _MessageRowState(); _MessageRowState createState() => _MessageRowState();
@ -31,8 +31,10 @@ class _MessageRowState extends State<MessageRow> {
fromMe = false; fromMe = false;
} }
Widget wdgBubble = Widget wdgBubble = Flexible(
Flexible(flex: 3, fit: FlexFit.loose, child: Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble()); flex: 3,
fit: FlexFit.loose,
child: malformed ? MalformedBubble() : (Provider.of<MessageState>(context).loaded == true ? widgetForOverlay(Provider.of<MessageState>(context).overlay) : MessageLoadingBubble()));
Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor()); Widget wdgIcons = Icon(Icons.delete_forever_outlined, color: Provider.of<Settings>(context).theme.dropShadowColor());
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10)); Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
var widgetRow = <Widget>[]; var widgetRow = <Widget>[];
@ -50,12 +52,10 @@ class _MessageRowState extends State<MessageRow> {
child: Padding( child: Padding(
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: ProfileImage( child: ProfileImage(
diameter: 48.0, diameter: 48.0,
imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath, imagePath: Provider.of<MessageState>(context).senderImage ?? contact.imagePath,
//maskOut: contact.status != "Authenticated", //maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())));
badgeTextColor: Colors.red, badgeColor: Colors.red,
)));
widgetRow = <Widget>[ widgetRow = <Widget>[
wdgPortrait, wdgPortrait,
@ -76,7 +76,7 @@ class _MessageRowState extends State<MessageRow> {
case 101: case 101:
return InvitationBubble(); return InvitationBubble();
} }
return MalformedBubble(); return null;
} }
void _btnAdd() { void _btnAdd() {
@ -94,10 +94,7 @@ class _MessageRowState extends State<MessageRow> {
final setPeerAttributeJson = jsonEncode(setPeerAttribute); final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson); Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar( final snackBar = SnackBar(content: Text(AppLocalizations.of(context).successfullAddedContact));
content: Text(AppLocalizations.of(context)!.successfullAddedContact),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
} }

View File

@ -1,58 +1,29 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../settings.dart'; import '../settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// Provides a styled Password Input Field for use in Form Widgets. // Provides a styled Password Input Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchPasswordField extends StatefulWidget { class CwtchPasswordField extends StatefulWidget {
CwtchPasswordField({required this.controller, required this.validator, this.action, this.autofocus = false}); CwtchPasswordField({this.controller, this.validator});
final TextEditingController controller; final TextEditingController controller;
final FormFieldValidator validator; final FormFieldValidator validator;
final Function(String)? action;
final bool autofocus;
@override @override
_CwtchTextFieldState createState() => _CwtchTextFieldState(); _CwtchTextFieldState createState() => _CwtchTextFieldState();
} }
class _CwtchTextFieldState extends State<CwtchPasswordField> { class _CwtchTextFieldState extends State<CwtchPasswordField> {
bool obscureText = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// todo: translations
var label = AppLocalizations.of(context)!.tooltipShowPassword;
if (!obscureText) {
label = AppLocalizations.of(context)!.tooltipHidePassword;
}
return Consumer<Settings>(builder: (context, theme, child) { return Consumer<Settings>(builder: (context, theme, child) {
return TextFormField( return TextFormField(
autofocus: widget.autofocus,
controller: widget.controller, controller: widget.controller,
validator: widget.validator, validator: widget.validator,
obscureText: obscureText, obscureText: true,
autovalidateMode: AutovalidateMode.always,
onFieldSubmitted: widget.action,
textInputAction: TextInputAction.unspecified,
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
decoration: InputDecoration( decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscureText = !obscureText;
});
},
icon: Icon((obscureText ? CwtchIcons.eye_closed : CwtchIcons.eye_open), semanticLabel: label),
tooltip: label,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),
focusColor: theme.current().defaultButtonActiveColor(),
splashColor: theme.current().defaultButtonActiveColor(),
),
errorStyle: TextStyle( errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(), color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -64,6 +35,7 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
fillColor: theme.current().textfieldBackgroundColor(), fillColor: theme.current().textfieldBackgroundColor(),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
), ),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
); );
}); });
} }

View File

@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import '../settings.dart'; import '../settings.dart';
class ProfileImage extends StatefulWidget { class ProfileImage extends StatefulWidget {
ProfileImage({required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false}); ProfileImage({this.imagePath, this.diameter, this.border, this.badgeCount = 0, this.badgeColor, this.badgeTextColor, this.maskOut = false});
final double diameter; final double diameter;
final String imagePath; final String imagePath;
final Color border; final Color border;
@ -21,8 +21,7 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> { class _ProfileImageState extends State<ProfileImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RepaintBoundary( return Stack(children: [
child: Stack(children: [
ClipOval( ClipOval(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Container( child: Container(
@ -38,7 +37,7 @@ class _ProfileImageState extends State<ProfileImage> {
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute // We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut colorBlendMode: !widget.maskOut
? Provider.of<Settings>(context).theme.identifier() == "dark" ? Provider.of<Settings>(context).theme == Opaque.dark
? BlendMode.softLight ? BlendMode.softLight
: BlendMode.darken : BlendMode.darken
: BlendMode.srcOut, : BlendMode.srcOut,
@ -58,6 +57,6 @@ class _ProfileImageState extends State<ProfileImage> {
child: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)), child: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)),
), ),
)), )),
])); ]);
} }
} }

View File

@ -30,96 +30,84 @@ class _ProfileRowState extends State<ProfileRow> {
padding: const EdgeInsets.all(2.0), //border size padding: const EdgeInsets.all(2.0), //border size
child: ProfileImage( child: ProfileImage(
badgeCount: 0, badgeCount: 0,
badgeColor: Provider badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor(),
.of<Settings>(context) badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor(),
.theme
.portraitProfileBadgeColor(),
badgeTextColor: Provider
.of<Settings>(context)
.theme
.portraitProfileBadgeTextColor(),
diameter: 64.0, diameter: 64.0,
imagePath: profile.imagePath, imagePath: profile.imagePath,
border: profile.isOnline ? Provider border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())),
.of<Settings>(context)
.theme
.portraitOnlineBorderColor() : Provider
.of<Settings>(context)
.theme
.portraitOfflineBorderColor())),
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [
Text( Text(
profile.nickname, profile.nickname,
semanticsLabel: profile.nickname, semanticsLabel: profile.nickname,
style: Provider style: Provider.of<FlwtchState>(context).biggerFont,
.of<FlwtchState>(context) softWrap: true,
.biggerFont, overflow: TextOverflow.ellipsis,
softWrap: true, ),
overflow: TextOverflow.ellipsis, ExcludeSemantics(
), child: Text(
ExcludeSemantics( profile.onion,
child: Text( softWrap: true,
profile.onion, overflow: TextOverflow.ellipsis,
softWrap: true, ))
overflow: TextOverflow.ellipsis, ],
)) )),
],
)),
IconButton( IconButton(
enableFeedback: true, enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname, tooltip: AppLocalizations.of(context).editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()), icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () { onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted); _pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath);
}, },
) )
], ],
), ),
onTap: () { onTap: () {
setState(() { setState(() {
var appState = Provider.of<AppState>(context, listen: false); var flwtch = Provider.of<FlwtchState>(context, listen: false);
appState.selectedProfile = profile.onion; flwtch.cwtch.SelectProfile(profile.onion);
appState.selectedConversation = null; flwtch.setState(() {
flwtch.selectedProfile = profile;
flwtch.selectedConversation = "";
});
_pushContactList(profile, appState.isLandscape(context));//orientation == Orientation.landscape); switch (flwtch.columns.length) {
case 1:
_pushContactList(profile, false);
break;
case 2:
_pushContactList(profile, true);
break;
} // case 3: handled by TripleColumnView
}); });
}, },
)); ));
} }
void _pushContactList(ProfileInfoState profile, bool isLandscape) { void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute<void>( MaterialPageRoute<void>(
settings: RouteSettings(name: "conversations"),
builder: (BuildContext buildcontext) { builder: (BuildContext buildcontext) {
return OrientationBuilder( return MultiProvider(
builder: (orientationBuilderContext, orientation) { providers: [
return MultiProvider( ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
providers: [ ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ],
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList), builder: (context, widget) => includeDoublePane ? DoubleColumnView() : ContactsView(),
], );
builder: (innercontext, widget) {
var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context);
return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : ContactsView();
}
);
});
}, },
), ),
); );
} }
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) { void _pushAddEditProfile({onion: "", displayName: "", profileImage: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider<ProfileInfoState>( ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted), create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage),
), ),
], ],
builder: (context, widget) => AddEditProfileView(), builder: (context, widget) => AddEditProfileView(),

View File

@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// From https://github.com/flutter/flutter/issues/75675#issuecomment-846601115
// necessary to fix bug in flutter engine on Windows.
// todo: hopefully we can remove this soon
class ShiftRightFixer extends StatefulWidget {
ShiftRightFixer({required this.child});
final Widget child;
@override
State<StatefulWidget> createState() => _ShiftRightFixerState();
}
class _ShiftRightFixerState extends State<ShiftRightFixer> {
final FocusNode focus = FocusNode(skipTraversal: true, canRequestFocus: false);
@override
Widget build(BuildContext context) {
return Focus(
focusNode: focus,
onKey: (_, RawKeyEvent event) {
return event.physicalKey == PhysicalKeyboardKey.shiftRight ? KeyEventResult.handled : KeyEventResult.ignored;
},
child: widget.child,
);
}
}

View File

@ -7,30 +7,17 @@ doNothing(String x) {}
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchTextField extends StatefulWidget { class CwtchTextField extends StatefulWidget {
CwtchTextField({required this.controller, required this.labelText, this.validator, this.autofocus = false, this.onChanged = doNothing}); CwtchTextField({this.controller, this.labelText, this.validator, this.onChanged = doNothing});
final TextEditingController controller; final TextEditingController controller;
final String labelText; final String labelText;
final FormFieldValidator? validator; final FormFieldValidator validator;
final Function(String) onChanged; final Function(String) onChanged;
final bool autofocus;
@override @override
_CwtchTextFieldState createState() => _CwtchTextFieldState(); _CwtchTextFieldState createState() => _CwtchTextFieldState();
} }
class _CwtchTextFieldState extends State<CwtchTextField> { class _CwtchTextFieldState extends State<CwtchTextField> {
late final FocusNode _focusNode;
@override
void initState() {
_focusNode = FocusNode();
_focusNode.addListener(() {
// Select all...
if (_focusNode.hasFocus) widget.controller.selection = TextSelection(baseOffset: 0, extentOffset: widget.controller.text.length);
});
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<Settings>(builder: (context, theme, child) { return Consumer<Settings>(builder: (context, theme, child) {
@ -38,8 +25,6 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
controller: widget.controller, controller: widget.controller,
validator: widget.validator, validator: widget.validator,
onChanged: widget.onChanged, onChanged: widget.onChanged,
autofocus: widget.autofocus,
focusNode: _focusNode,
decoration: InputDecoration( decoration: InputDecoration(
labelText: widget.labelText, labelText: widget.labelText,
labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
@ -55,6 +40,7 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
fillColor: theme.current().textfieldBackgroundColor(), fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
); );
}); });
} }

View File

@ -1,4 +1,3 @@
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.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';
@ -16,13 +15,16 @@ class TorIcon extends StatefulWidget {
class _TorIconState extends State<TorIcon> { class _TorIconState extends State<TorIcon> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RepaintBoundary( return Image(
child: Icon( image: AssetImage(Provider.of<TorStatus>(context).progress == 0
Provider.of<TorStatus>(context).progress == 0 ? CwtchIcons.onion_off : (Provider.of<TorStatus>(context).progress == 100 ? CwtchIcons.onion_on : CwtchIcons.onion_waiting), ? "assets/core/Tor_OFF.png"
: (Provider.of<TorStatus>(context).progress == 100 ? "assets/core/Tor_icon.png" : "assets/core/Tor_Booting_up.png")),
// Color the onion per the text color...
color: Provider.of<Settings>(context).theme.mainTextColor(), color: Provider.of<Settings>(context).theme.mainTextColor(),
colorBlendMode: BlendMode.srcIn,
semanticLabel: Provider.of<TorStatus>(context).progress == 100 semanticLabel: Provider.of<TorStatus>(context).progress == 100
? AppLocalizations.of(context)!.networkStatusOnline ? AppLocalizations.of(context).networkStatusOnline
: (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor), : (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context).networkStatusDisconnected : AppLocalizations.of(context).networkStatusAttemptingTor),
)); );
} }
} }

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class TorStatusLabel extends StatefulWidget {
@override
_TorStatusState createState() => _TorStatusState();
}
class _TorStatusState extends State<TorStatusLabel> {
String status = "";
@override
Widget build(BuildContext context) {
return Builder(
builder: (context2) => StreamBuilder<String>(
stream: Provider.of<FlwtchState>(context).appStatus.torStatus(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(
snapshot.hasData ? snapshot.data : AppLocalizations.of(context).loadingTor,
style: Theme.of(context).textTheme.headline4,
);
},
));
}
}

View File

@ -1,10 +1,9 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Cwtch Name=cwtch
Comment=Metadata Resistant Chat Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=./lib/ ./cwtch Exec=env LD_LIBRARY_PATH=./lib/ ./cwtch
Icon=cwtch Icon=cwtch
Terminal=false Terminal=false
Categories=Network;InstantMessaging; Categories=Internet;Chat;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat

View File

@ -1,10 +0,0 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Cwtch
Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/bin/cwtch
Icon=cwtch
Terminal=false
Categories=Network;InstantMessaging;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,10 +0,0 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Cwtch
Comment=Metadata Resistant Chat
Exec=env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/bin/cwtch
Icon=cwtch
Terminal=false
Categories=Network;InstantMessaging;
Keywords=Internet;IM;Instant Messaging;Messaging;Chat

View File

@ -2,8 +2,6 @@
// Generated file. Do not edit. // Generated file. Do not edit.
// //
// clang-format off
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"

View File

@ -2,8 +2,6 @@
// Generated file. Do not edit. // Generated file. Do not edit.
// //
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_ #ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_

View File

@ -1,17 +0,0 @@
#!/bin/sh
mkdir -p ~/.local/bin
cp cwtch ~/.local/bin/
mkdir -p ~/.local/share/icons
cp cwtch.png ~/.local/share/icons
mkdir -p ~/.local/share/cwtch
cp -r data ~/.local/share/cwtch
mkdir -p ~/.local/lib/cwtch
cp -r lib/* ~/.local/lib/cwtch
mkdir -p ~/.local/share/applications
sed "s|~|$HOME|g" cwtch.home.desktop > $HOME/.local/share/applications/cwtch.desktop

View File

@ -1,13 +0,0 @@
#!/bin/sh
cp cwtch /usr/bin/
cp cwtch.png /usr/share/icons
mkdir -p /usr/share/cwtch
cp -r data /usr/share/cwtch
mkdir -p /usr/lib/cwtch
cp -r lib/* /usr/lib/cwtch
cp cwtch.sys.desktop /usr/share/applications/cwtch.desktop

View File

@ -1,14 +1,5 @@
#include "my_application.h" #include "my_application.h"
// Added to check for location of assets folder
#include <sys/types.h>
#include <sys/stat.h>
// To get the home dir of the user
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <flutter_linux/flutter_linux.h> #include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11 #ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h> #include <gdk/gdkx.h>
@ -21,18 +12,6 @@ struct _MyApplication {
char** dart_entrypoint_arguments; char** dart_entrypoint_arguments;
}; };
// Redefining from flutter/engine::shell/platform/linux/fl_dart_project.cc
// struct def required here to enable compiler to allow access to variables
struct _FlDartProject {
GObject parent_instance;
gboolean enable_mirrors;
gchar* aot_library_path;
gchar* assets_path;
gchar* icu_data_path;
gchar** dart_entrypoint_args;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate. // Implements GApplication::activate.
@ -69,36 +48,11 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_title(window, "cwtch"); gtk_window_set_title(window, "cwtch");
} }
gtk_window_set_icon_from_file(window, "./cwtch.png", NULL);
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlDartProject) project = fl_dart_project_new();
// Check if assets folder is relative to the executable or if we can use a system copy
struct stat info;
if (stat(fl_dart_project_get_assets_path(project), &info ) != 0 ) {
if( stat("/usr/share/cwtch/data/flutter_assets", &info ) != 0 ) {
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
// /home/$USER/.local/share/cwtch/data/flutter_assets
project->assets_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "flutter_assets", nullptr);
// /home/$USER/.local/lib/cwtch/
project->aot_library_path = g_build_filename(homedir, ".local", "lib", "cwtch", "libapp.so", nullptr);
// /home/$USER/.local/share/cwtch/data
project->icu_data_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "icudtl.dat", nullptr);
gtk_window_set_icon_from_file(window, g_build_filename(homedir, ".local", "share", "icons", "cwtch.png", nullptr), NULL);
} else {
// /usr/share/cwtch/data/flutter_assets
project->assets_path = g_build_filename("/", "usr", "share", "cwtch", "data", "flutter_assets", nullptr);
// /usr/lib/cwtch
project->aot_library_path = g_build_filename("/", "usr", "lib", "cwtch", "libapp.so", nullptr);
// /usr/share/cwtch/data
project->icu_data_path = g_build_filename("/", "usr", "share", "cwtch", "data", "icudtl.dat", nullptr);
gtk_window_set_icon_from_file(window, "/usr/share/icons/cwtch.png", NULL);
}
} else {
gtk_window_set_icon_from_file(window, "./cwtch.png", NULL);
}
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project); FlView* view = fl_view_new(project);

View File

@ -1,27 +1,20 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
ansicolor: archive:
dependency: transitive dependency: transitive
description: description:
name: ansicolor name: archive
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "3.1.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
async: async:
dependency: transitive dependency: transitive
description: description:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.7.0" version: "2.6.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -42,7 +35,7 @@ packages:
name: charcode name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.2.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -57,27 +50,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.2"
dbus:
dependency: transitive
description:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
desktop_notifications:
dependency: "direct main"
description:
name: desktop_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -98,12 +84,17 @@ packages:
name: file name: file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.1" version: "6.1.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_driver:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -119,20 +110,25 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob: glob:
dependency: "direct main" dependency: "direct main"
description: description:
name: glob name: glob
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "2.0.1"
http: http:
dependency: transitive dependency: transitive
description: description:
name: http name: http
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.13.3" version: "0.13.1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -140,13 +136,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
injector: integration_test:
dependency: transitive dependency: "direct main"
description: description:
name: injector name: integration_test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "1.0.2+3"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -174,14 +170,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.0" version: "1.3.0"
msix:
dependency: "direct dev"
description:
name: msix
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -189,69 +178,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.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.0.1" version: "1.0.0"
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.2" version: "1.0.0"
package_info_plus_macos: package_info_plus_macos:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_macos name: package_info_plus_macos
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.0.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.0"
package_info_plus_web: package_info_plus_web:
dependency: transitive dependency: transitive
description: description:
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.1" version: "1.0.0"
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.1" version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -293,7 +261,7 @@ packages:
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.1" version: "2.0.0"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -301,13 +269,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.11.0" version: "1.11.0"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -335,7 +296,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.0" version: "4.3.2+3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -369,6 +330,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
sync_http:
dependency: transitive
description:
name: sync_http
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -382,7 +350,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.1" version: "0.3.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -397,13 +365,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.0.5"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -411,20 +393,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.2"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks: sdks:
dart: ">=2.13.0 <3.0.0" dart: ">=2.12.0 <3.0.0"
flutter: ">=1.20.0" flutter: ">=1.20.0"

View File

@ -15,15 +15,15 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+15 version: 1.0.0+1
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.7.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
provider: 5.0.0 provider: "4.3.2+3"
package_info_plus: ^1.0.0 package_info_plus: ^1.0.0
#intl_translation: any #intl_translation: any
flutter_localizations: flutter_localizations:
@ -34,21 +34,26 @@ dependencies:
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
ffi: ^1.0.0 ffi: ^1.0.0
path_provider: ^2.0.0 path_provider: ^2.0.0
desktop_notifications: 0.5.0
glob: any glob: any
# todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829
# testing-related deps
integration_test: ^1.0.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_driver:
sdk: flutter
dev_dependencies:
msix: ^2.1.3
# Uncomment to update lokalise translations (see README for list of deps to comment out bc incompatibilities)
#dev_dependencies: #dev_dependencies:
# flutter_lokalise: any # flutter_lokalise: any
#flutter_lokalise:
# project_id: "737094205fceda35c50aa2.60364948"
# api_token: "0407300fe4aa1edf1c1818e56234589e74c83c59" # Read only api Token from Dan
# alternatively: flutter pub run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/intl/app_localizations.dart lib/l10n/intl_*.arb --api-token X --project-id Y
#flutter_lokalise:
# project_id: ""
# api_token: ""
# include_tags:
# - tag1
# - tag2
flutter_intl: flutter_intl:
enabled: true enabled: true
@ -83,11 +88,6 @@ flutter:
- assets/profiles/ - assets/profiles/
- assets/servers/ - assets/servers/
fonts:
- family: CwtchIcons
fonts:
- asset: assets/fonts/CwtchIcons.ttf
# To add custom fonts to your application, add a fonts section here, # To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a # in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a # "family" key with the font family name, and a "fonts" key with a
@ -107,19 +107,3 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages
msix_config:
display_name: Cwtch
publisher_display_name: Open Privacy Research Society
identity_name: im.cwtch.flwtch
msix_version: 1.0.0.0
certificate_path: codesign.pfx
certificate_password: pfx_pass
publisher: CN=Open Privacy Research Society, O=Open Privacy Research Society, L=Vancouver, S=British Columbia, C=CA
logo_path: cwtch.png
start_menu_icon_path: cwtch.png
tile_icon_path: assets\cwtch_title.png
icons_background_color: transparent
architecture: x64
capabilities: 'internetClient'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 B

After

Width:  |  Height:  |  Size: 246 B

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark()); var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight()); var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark); ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
void main() { void main() {
@ -39,7 +39,7 @@ void main() {
home: Card(child: CwtchButtonTextField( home: Card(child: CwtchButtonTextField(
icon: Icon(Icons.bug_report_outlined), icon: Icon(Icons.bug_report_outlined),
tooltip: testingStr, tooltip: testingStr,
controller: ctrlr1, onPressed: () { }, controller: ctrlr1,
)), )),
);} );}
)); ));

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark()); var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight()); var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark); ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
void main() { void main() {

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark()); var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight()); var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark); ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
String file(String slug) { String file(String slug) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -14,8 +14,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var settingsEnglishDark = Settings(Locale("en", ''), OpaqueDark()); var settingsEnglishDark = Settings(Locale("en", ''), Opaque.dark);
var settingsEnglishLight = Settings(Locale("en", ''), OpaqueLight()); var settingsEnglishLight = Settings(Locale("en", ''), Opaque.light);
ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark); ChangeNotifierProvider<Settings> getSettingsEnglishDark() => ChangeNotifierProvider.value(value: settingsEnglishDark);
String file(String slug) { String file(String slug) {
@ -27,7 +27,7 @@ void main() {
tester.binding.window.physicalSizeTestValue = Size(800, 300); tester.binding.window.physicalSizeTestValue = Size(800, 300);
final TextEditingController ctrlr1 = TextEditingController(); final TextEditingController ctrlr1 = TextEditingController();
Widget testWidget = CwtchTextField(controller: ctrlr1, validator: (value) { }, labelText: '',); Widget testWidget = CwtchTextField(controller: ctrlr1);
Widget testHarness = MultiProvider( Widget testHarness = MultiProvider(
providers:[getSettingsEnglishDark()], providers:[getSettingsEnglishDark()],
@ -76,7 +76,7 @@ void main() {
if (number == null) return strFail2; if (number == null) return strFail2;
return null; return null;
}, },
onChanged: (value) => formKey.currentState!.validate(), onChanged: (value) => formKey.currentState.validate(),
); );
Widget testHarness = MultiProvider( Widget testHarness = MultiProvider(
@ -125,7 +125,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// empty string // empty string
formKey.currentState!.validate(); //(ctrlr1.clear() doesn't trigger validate like keypress does) formKey.currentState.validate(); //(ctrlr1.clear() doesn't trigger validate like keypress does)
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('form_final'))); await expectLater(find.byWidget(testHarness), matchesGoldenFile(file('form_final')));
expect(find.text(strFail1), findsOneWidget); expect(find.text(strFail1), findsOneWidget);

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.15) cmake_minimum_required(VERSION 3.15)
project(cwtch LANGUAGES CXX) project(flutter_app LANGUAGES CXX)
set(BINARY_NAME "cwtch") set(BINARY_NAME "flutter_app")
cmake_policy(SET CMP0063 NEW) cmake_policy(SET CMP0063 NEW)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,6 +0,0 @@
- cp nsis/cwtch-installer.nsi deploy/
- cd deploy
- makensis -V3 cwtch-installer.nsi
- export BUILDDATE=`date +%G-%m-%d-%H-%M`
- export FILENAME=cwtch-installer-$BUILDDATE.exe
- mv cwtch-installer.exe $FILENAME

View File

@ -1,92 +0,0 @@
; USAGE: Run in ui/deploy, requires the output be in 'windows' directory
!include "MUI2.nsh"
; General settings ----------------------------
Name "Cwtch"
; !define MUI_BRANDINGTEXT "SIG Beta Ver. 1.0"
Unicode True
# define the name of the installer
Outfile "cwtch-installer.exe"
# For removing Start Menu shortcut in Windows 7
#RequestExecutionLevel user
RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
# define the directory to install to, the desktop in this case as specified
# by the predefined $DESKTOP variable
InstallDir "$PROGRAMFILES\Cwtch"
;Get installation folder from registry if available
InstallDirRegKey HKCU "Software\Cwtch" "installLocation"
; MUI Interface -----------------------------
!define MUI_INSTALLCOLORS "DFB9DE 281831"
; 128x128, 32bit
!define MUI_ICON "../runner/resources/knot_128.ico"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "cwtch_title.bmp"
!define MUI_TEXTCOLOR "350052"
!define MUI_WELCOMEFINISHPAGE_BITMAP "brand_side.bmp"
!define MUI_WELCOMEFINISHPAGE_BITMAP_STRETCH NoStretchNoCrop
!define MUI_INSTFILESPAGE_COLORS "DFB9DE 281831"
!define MUI_INSTFILESPAGE_PROGRESSBAR "colored"
!define MUI_FINISHPAGE_NOAUTOCLOSE
ShowInstDetails show
; Pages --------
!define MUI_WELCOMEPAGE_TITLE "Welcome to the Cwtch installer"
!define MUI_WELCOMEPAGE_TEXT "Cwtch (pronounced: kutch) is a Welsh word roughly meaning 'a hug that creates a safe space'$\n$\n\
Cwtch is a platform for building consentful, decentralized, untrusted infrastructure using metadata resistant group communication applications. Currently there is a selfnamed instant messaging prototype app that is driving development and testing. Many Further apps are planned as the platform matures."
!define MUI_FINISHPAGE_TITLE "Enjoy Cwtch"
!define MUI_FINISHPAGE_RUN $INSTDIR/ui.exe
!define MUI_FINISHPAGE_TEXT "You can keep up-to-date on Cwtch and report any issues you have at https://cwtch.im"
!define MUI_FINISHPAGE_LINK "https://cwtch.im"
!define MUI_FINISHPAGE_LINK_LOCATION "https://cwtch.im"
!define MUI_FINISHPAGE_LINK_COLOR "D01972"
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "../../LICENSE"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
; Languages --------------------------------
!insertmacro MUI_LANGUAGE "English"
# default section
Section
# define the output path for this file
SetOutPath $INSTDIR
# define what to install and place it in the output path
# Filler for .sh to populate with contents of deploy/windows
#FILESLISTSTART
FILE /r "..\..\build\windows\runner\Release\"
#FILESLISTEND
# create a shortcut in the start menu programs directory
CreateDirectory "$SMPROGRAMS\Cwtch"
CreateShortcut "$SMPROGRAMS\Cwtch\Cwtch.lnk" "$INSTDIR\cwtch.exe" "" "$INSTDIR\cwtch.ico"
;Store installation folder
WriteRegStr HKCU "Software\Cwtch" "installLocation" $INSTDIR
SectionEnd

Some files were not shown because too many files have changed in this diff Show More