Compare commits
178 Commits
ios_build_
...
trunk
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | f030e8b573 | |
Dan Ballard | 3ed8b94274 | |
Sarah Jamie Lewis | 2c1347e50e | |
Dan Ballard | 40c27c3f30 | |
Sarah Jamie Lewis | d69966423a | |
Dan Ballard | 883b739b5c | |
Dan Ballard | 1d024ac63e | |
Dan Ballard | b34ffcd211 | |
Sarah Jamie Lewis | 4dea1e1dd4 | |
erinn | 6a5309427f | |
erinn | 6d261c999b | |
erinn | 00ce310cbc | |
erinn | 946d7accb2 | |
erinn | 996cad4ece | |
Sarah Jamie Lewis | b9549e6885 | |
Dan Ballard | e01cea3238 | |
Sarah Jamie Lewis | d553d6a474 | |
Sarah Jamie Lewis | d0770b4bd8 | |
Sarah Jamie Lewis | 07b97fdb02 | |
Dan Ballard | ff28f37471 | |
Nima Boscarino | 1c03fdf1db | |
Nima Boscarino | a58e09dec5 | |
Nima Boscarino | 35dcc24e66 | |
Nima Boscarino | b8d50f234a | |
Nima Boscarino | ec1dd05ba1 | |
Sarah Jamie Lewis | edfc070d9a | |
Dan Ballard | d3da68272b | |
Dan Ballard | 44b77f0a90 | |
Sarah Jamie Lewis | 10584984c3 | |
Sarah Jamie Lewis | 0d4c67923d | |
Dan Ballard | e9044daab3 | |
Dan Ballard | 659b8d5bf1 | |
Sarah Jamie Lewis | 0dc4849a5d | |
erinn | 626aa386d0 | |
erinn | 8653a31482 | |
erinn | 10838ba67a | |
erinn | d581718d36 | |
erinn | e55d9301e4 | |
Sarah Jamie Lewis | f7f2ca6621 | |
erinn | 6331a6da95 | |
Dan Ballard | 9fe7ee4b56 | |
Dan Ballard | fb18a0e3b8 | |
Dan Ballard | 87f7c2c29b | |
Sarah Jamie Lewis | 07b00ee1d0 | |
Sarah Jamie Lewis | 05433397d4 | |
Dan Ballard | fc59bd332b | |
Dan Ballard | 3ac8c32afe | |
Dan Ballard | 2c57499478 | |
Dan Ballard | a9bcf13c1f | |
Dan Ballard | 82294eb6af | |
Dan Ballard | 60a4775f4d | |
Dan Ballard | 3e75c4d106 | |
Dan Ballard | e9407ae426 | |
Sarah Jamie Lewis | c5f154a25f | |
Sarah Jamie Lewis | cae44cbd46 | |
Dan Ballard | b65d16aa8a | |
Dan Ballard | ed8292ece9 | |
Dan Ballard | 562c05183b | |
Dan Ballard | c304e2ec2a | |
Dan Ballard | 9789a42e94 | |
Dan Ballard | d4b9f1dc55 | |
erinn | 2b8f8e825f | |
Sarah Jamie Lewis | 73690e8bac | |
Sarah Jamie Lewis | 088cebcc6b | |
Sarah Jamie Lewis | fe156108b5 | |
Dan Ballard | 4971879b0e | |
Dan Ballard | 9403a1c540 | |
Sarah Jamie Lewis | d4811407b2 | |
erinn | c2297d5b5d | |
Sarah Jamie Lewis | 10197dc300 | |
erinn | 368515423b | |
erinn | 74e25534c5 | |
Sarah Jamie Lewis | e36f04095f | |
Dan Ballard | c174f2f293 | |
Dan Ballard | d070b7141e | |
Sarah Jamie Lewis | 0cdf18df79 | |
Sarah Jamie Lewis | 222e2c6f36 | |
Dan Ballard | e9514f0296 | |
Sarah Jamie Lewis | 175bfefa4e | |
Sarah Jamie Lewis | 77fdecc748 | |
Sarah Jamie Lewis | 0c81b4f9d0 | |
Sarah Jamie Lewis | 9556150c05 | |
Sarah Jamie Lewis | efbf7f5bff | |
Sarah Jamie Lewis | 3240e41d49 | |
Sarah Jamie Lewis | bf31a2b062 | |
erinn | d8453bc530 | |
erinn | 01d816209b | |
erinn | a9cc4b7425 | |
erinn | d4aa1cb397 | |
erinn | 8fe577afd4 | |
Sarah Jamie Lewis | b972cfd45e | |
Dan Ballard | 6d79c1dc17 | |
erinn | cb3c161277 | |
erinn | 4eed72ded3 | |
erinn | 78ea12dff3 | |
Sarah Jamie Lewis | ac4e0c1679 | |
erinn | 4f614b69fd | |
erinn | e7c6bb145a | |
erinn | 77ea122ff3 | |
Sarah Jamie Lewis | affc48089d | |
Sarah Jamie Lewis | 539b93836a | |
Dan Ballard | 6b33e129be | |
Sarah Jamie Lewis | c1aee0d128 | |
Sarah Jamie Lewis | 3a34044f8e | |
Dan Ballard | be2de14d65 | |
Dan Ballard | ac619cd514 | |
Dan Ballard | 34da2bea35 | |
Sarah Jamie Lewis | f3b09d3e3a | |
Dan Ballard | 7de7e36e99 | |
Dan Ballard | c172a814bf | |
Dan Ballard | afe448b43e | |
Trevor Bergeron | d85aac0a13 | |
Dan Ballard | 4c46620733 | |
Dan Ballard | 652892af4c | |
Sarah Jamie Lewis | dc69d34484 | |
Sarah Jamie Lewis | 9abee953a6 | |
fyne | 86dd8d7f78 | |
Trevor Bergeron | 34793ca459 | |
Dan Ballard | cafac4aa19 | |
Dan Ballard | 43b7872438 | |
Sarah Jamie Lewis | bc24c7f3c7 | |
Dan Ballard | 47510be645 | |
fyne | be0c4f4d64 | |
fyne | b53f43d946 | |
fyne | 13f76d1861 | |
Trevor Bergeron | 10b43a5ff6 | |
Sarah Jamie Lewis | 7737085a28 | |
Dan Ballard | f1944dbc9e | |
Dan Ballard | 99762c0b29 | |
Dan Ballard | 5764e2d725 | |
Sarah Jamie Lewis | 476bdb626c | |
Dan Ballard | e648068c64 | |
Dan Ballard | a615f30eea | |
Dan Ballard | 851e391666 | |
Dan Ballard | d461bf879c | |
Dan Ballard | ca83033997 | |
Sarah Jamie Lewis | 7b88415fab | |
erinn | d6b6069ef1 | |
erinn | 08635aec7f | |
Sarah Jamie Lewis | 98363a11b6 | |
erinn | 7849deda95 | |
Sarah Jamie Lewis | 31b3d48f44 | |
Sarah Jamie Lewis | 4e3bd696ed | |
erinn | 00e4ed30f0 | |
Sarah Jamie Lewis | 30ea12a9ce | |
erinn | 397971a181 | |
Sarah Jamie Lewis | 6210a64315 | |
Sarah Jamie Lewis | 825fb23992 | |
Sarah Jamie Lewis | a06815fa08 | |
erinn | 62a9402357 | |
Sarah Jamie Lewis | 67ed282394 | |
erinn | e487d0bd65 | |
erinn | 76c6a5f069 | |
erinn | 004e211d0b | |
Sarah Jamie Lewis | afe2457062 | |
Dan Ballard | 42ed710fc9 | |
Sarah Jamie Lewis | 1a80b4b808 | |
erinn | 61d1a60b0e | |
Matthew van Eerde (^_^) | 7d15c41aed | |
Matthew van Eerde (^_^) | f09c40d7dd | |
erinn | b78f138cb2 | |
Sarah Jamie Lewis | 6d4ab5bab3 | |
Sarah Jamie Lewis | 3cf81e41d6 | |
Dan Ballard | ebb665355c | |
Dan Ballard | aee861df90 | |
Sarah Jamie Lewis | 132f1718ed | |
Dan Ballard | 5a8a39c77f | |
Sarah Jamie Lewis | e4d9829e7b | |
erinn | 06075583ed | |
erinn | 62bedcd612 | |
erinn | b3f9f10cdd | |
erinn | 65811231a7 | |
erinn | 0615a81042 | |
Sarah Jamie Lewis | 587bb783aa | |
Sarah Jamie Lewis | 94396d965b | |
Sarah Jamie Lewis | a19deb2b4d | |
Sarah Jamie Lewis | 505bceb887 | |
Dan Ballard | 83c9adac5d |
128
.drone.yml
|
@ -8,7 +8,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: cirrusci/flutter:dev
|
||||
image: cirrusci/flutter:2.5.3
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -20,11 +20,11 @@ steps:
|
|||
# force by pass of ssh host key check, less secure
|
||||
- ssh-keyscan -H git.openprivacy.ca >> ~/.ssh/known_hosts
|
||||
# use Drone ssh var instead of hardcode to allow forks to build (gogs@git.openprivacy.ca:cwtch.im/cwtch-ui.git)
|
||||
- git clone $DRONE_GIT_SSH_URL .
|
||||
- git clone gogs@git.openprivacy.ca:$DRONE_REPO.git .
|
||||
- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: cirrusci/flutter:dev
|
||||
image: cirrusci/flutter:2.5.3
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -47,7 +47,7 @@ steps:
|
|||
# #Todo: fix all the lint errors and add `-set_exit_status` above to enforce linting
|
||||
|
||||
- name: build-linux
|
||||
image: openpriv/flutter-desktop:linux-dev
|
||||
image: openpriv/flutter-desktop:linux-fstable-2.5.3
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -61,7 +61,7 @@ steps:
|
|||
- rm -r cwtch
|
||||
|
||||
- name: test-build-android
|
||||
image: cirrusci/flutter:dev
|
||||
image: cirrusci/flutter:2.5.3
|
||||
when:
|
||||
event: pull_request
|
||||
volumes:
|
||||
|
@ -71,7 +71,7 @@ steps:
|
|||
- flutter build apk --debug
|
||||
|
||||
- name: build-android
|
||||
image: cirrusci/flutter:dev
|
||||
image: cirrusci/flutter:2.5.3
|
||||
when:
|
||||
event: push
|
||||
environment:
|
||||
|
@ -95,7 +95,7 @@ steps:
|
|||
#- cp build/app/outputs/flutter-apk/app-debug.apk deploy/android
|
||||
|
||||
- name: widget-tests
|
||||
image: cirrusci/flutter:dev
|
||||
image: cirrusci/flutter:2.5.3
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /root/.pub-cache
|
||||
|
@ -117,7 +117,7 @@ steps:
|
|||
- echo $BUILDFILES_KEY > ~/id_rsab64
|
||||
- base64 -d ~/id_rsab64 > ~/id_rsa
|
||||
- chmod 400 ~/id_rsa
|
||||
- export DIR=flwtch-`cat VERSION`-`cat BUILDDATE`
|
||||
- export DIR=flwtch-`cat BUILDDATE`-`cat VERSION`
|
||||
- mv deploy $DIR
|
||||
- cp -r coverage/html $DIR/coverage-tests
|
||||
- cp -r test/failures $DIR/test-failures || true
|
||||
|
@ -125,8 +125,7 @@ steps:
|
|||
- 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/
|
||||
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
|
||||
- name: notify-email
|
||||
image: drillster/drone-email
|
||||
|
@ -154,7 +153,7 @@ volumes:
|
|||
temp: {}
|
||||
|
||||
trigger:
|
||||
repo: cwtch.im/cwtch-ui
|
||||
#repo: cwtch.im/cwtch-ui # allow forks to build?
|
||||
branch: trunk
|
||||
event:
|
||||
- push
|
||||
|
@ -175,7 +174,7 @@ clone:
|
|||
|
||||
steps:
|
||||
- name: clone
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
|
||||
environment:
|
||||
buildbot_key_b64:
|
||||
from_secret: buildbot_key_b64
|
||||
|
@ -187,22 +186,22 @@ steps:
|
|||
- git init
|
||||
# -o UserKnownHostsFile=../known_hosts
|
||||
- git config core.sshCommand 'ssh -o StrictHostKeyChecking=no -i ../id_rsa'
|
||||
- git remote add origin $Env:DRONE_GIT_SSH_URL
|
||||
- git remote add origin gogs@git.openprivacy.ca:$Env:DRONE_REPO.git
|
||||
- git pull origin trunk
|
||||
- git fetch --tags
|
||||
- git checkout $Env:DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
|
||||
commands:
|
||||
- powershell -command "Invoke-WebRequest -Uri https://dist.torproject.org/torbrowser/10.5a17/tor-win64-0.4.6.5.zip -OutFile tor.zip"
|
||||
- powershell -command "Invoke-WebRequest -Uri https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.zip -OutFile tor.zip"
|
||||
- powershell -command "if ((Get-FileHash tor.zip -Algorithm sha512).Hash -ne '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }"
|
||||
- git describe --tags --abbrev=1 > VERSION
|
||||
- powershell -command "Get-Date -Format 'yyyy-MM-dd-HH-mm'" > BUILDDATE
|
||||
- .\fetch-libcwtch-go.ps1
|
||||
|
||||
- name: build-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
|
||||
commands:
|
||||
- flutter pub get
|
||||
- $Env:version += type .\VERSION
|
||||
|
@ -213,9 +212,9 @@ steps:
|
|||
# flutter hasn't worked out it's packaging of required dll's so we have to resort to this manual nonsense
|
||||
# https://github.com/google/flutter-desktop-embedding/issues/587
|
||||
# https://github.com/flutter/flutter/issues/53167
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30036\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\vcruntime140.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\vcruntime140_1.dll $Env:releasedir
|
||||
- copy C:\BuildTools\VC\Redist\MSVC\14.29.30133\x64\Microsoft.VC142.CRT\msvcp140.dll $Env:releasedir
|
||||
- copy README.md $Env:releasedir\
|
||||
- copy windows\*.bat $Env:releasedir\
|
||||
- powershell -command "Expand-Archive -Path tor.zip -DestinationPath $Env:releasedir\Tor"
|
||||
|
@ -238,7 +237,7 @@ steps:
|
|||
- $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:buildname = 'flwtch-win-' + $Env:builddate + '-' + $Env:version
|
||||
- $Env:builddir = $Env:buildname
|
||||
- echo $Env:pfx > codesign.pfx.b64
|
||||
- certutil -decode codesign.pfx.b64 codesign.pfx
|
||||
|
@ -258,7 +257,7 @@ steps:
|
|||
- move *.sha512 deploy\$Env:builddir
|
||||
|
||||
- name: deploy-windows
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fdev2.3rc
|
||||
image: openpriv/flutter-desktop:windows-sdk30-fstable2.5.3
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
|
@ -268,11 +267,94 @@ steps:
|
|||
commands:
|
||||
- echo $Env:BUILDFILES_KEY > id_rsab64
|
||||
- certutil -decode id_rsab64 id_rsa
|
||||
- scp -r -o StrictHostKeyChecking=no -i id_rsa deploy\\* buildfiles@openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
- scp -r -o StrictHostKeyChecking=no -i id_rsa deploy\\* buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
|
||||
trigger:
|
||||
repo: cwtch.im/cwtch-ui
|
||||
# repo: cwtch.im/cwtch-ui # allow forks to build?
|
||||
branch: trunk
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: exec
|
||||
name: macos
|
||||
|
||||
platform:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: clone
|
||||
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 init
|
||||
- git config core.sshCommand 'ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa'
|
||||
- git remote add origin gogs@git.openprivacy.ca:$DRONE_REPO.git
|
||||
- git pull origin trunk
|
||||
- git fetch --tags
|
||||
- git checkout $DRONE_COMMIT
|
||||
# use Drone ssh var instead of hardcode to allow forks to build (gogs@git.openprivacy.ca:cwtch.im/cwtch-ui.git)
|
||||
#- git clone gogs@git.openprivacy.ca:$DRONE_REPO.git .
|
||||
#- git checkout $DRONE_COMMIT
|
||||
|
||||
- name: fetch
|
||||
commands:
|
||||
- ./fetch-tor-macos.sh
|
||||
- echo `git describe --tags --abbrev=1` > VERSION
|
||||
- echo `date +%G-%m-%d-%H-%M` > BUILDDATE
|
||||
- export PATH=$PATH:/Users/Dan/development/flutter/bin
|
||||
- flutter pub get
|
||||
- mkdir deploy
|
||||
- ./fetch-libcwtch-go-macos.sh
|
||||
- gem install --user-install cocoapods
|
||||
|
||||
- name: build-macos
|
||||
commands:
|
||||
- export PATH=$PATH:/Users/Dan/development/flutter/bin
|
||||
- export GEM_HOME=$HOME/.gem
|
||||
- export PATH=$GEM_HOME/ruby/2.6.0/bin:$PATH
|
||||
- flutter config --enable-macos-desktop
|
||||
- flutter build macos --dart-define BUILD_VER=`cat VERSION` --dart-define BUILD_DATE=`cat BUILDDATE`
|
||||
- export PATH=$PATH:/usr/local/bin #create-dmg
|
||||
- macos/package-release.sh
|
||||
- mkdir -p deploy
|
||||
- mv Cwtch.dmg deploy
|
||||
|
||||
- name: deploy-buildfiles
|
||||
environment:
|
||||
BUILDFILES_KEY:
|
||||
from_secret: buildfiles_key
|
||||
when:
|
||||
event: push
|
||||
status: [ success ]
|
||||
commands:
|
||||
- echo $BUILDFILES_KEY > ~/id_rsab64
|
||||
- base64 -d ~/id_rsab64 > ~/id_rsa
|
||||
- chmod 400 ~/id_rsa
|
||||
- export DIR=flwtch-macos-`cat BUILDDATE`-`cat VERSION`
|
||||
- mv deploy $DIR
|
||||
- cd $DIR
|
||||
- find . -type f -exec shasum -a 512 {} \; > ./../sha512s.txt
|
||||
- mv ./../sha512s.txt .
|
||||
- cd ..
|
||||
- scp -r -o StrictHostKeyChecking=no -i ~/id_rsa $DIR buildfiles@build.openprivacy.ca:/home/buildfiles/buildfiles/
|
||||
|
||||
trigger:
|
||||
#repo: cwtch.im/cwtch-ui # allow forks to build?
|
||||
branch: trunk
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
|
@ -40,8 +40,10 @@ app.*.symbols
|
|||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
libCwtch.so
|
||||
linux/tor
|
||||
linux/libCwtch.so
|
||||
android/cwtch/cwtch.aar
|
||||
android/app/src/main/jniLibs/*/libtor.so
|
||||
coverage
|
||||
test/failures
|
||||
.gradle
|
||||
.gradle
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
2021-11-09-18-25-v1.4.2
|
|
@ -1 +1 @@
|
|||
v1.1.0-2021-07-15-19-15
|
||||
2021-11-09-23-25-v1.4.2
|
43
README.md
|
@ -12,40 +12,65 @@ This README covers build instructions, for information on Cwtch itself please go
|
|||
- `install.home.sh` installs the app into your home directory
|
||||
- `install.sys.sh` as root to install system wide
|
||||
- or run out of the unziped directory
|
||||
- MacOS: Available from [https://cwtch.im/download/](https://cwtch.im/download/) as a .dmg
|
||||
|
||||
## Running
|
||||
|
||||
Cwtch processes the following environment variables:
|
||||
- `CWTCH_HOME=` overrides the default storage path of `~/.cwtch` with what ever you choose
|
||||
- `LOG_FILE=` will reroute all of libcwtch-go's logging to the specified file instead of the console
|
||||
- `LOG_FILE=` will reroute all of libcwtch-go's logging to the specified file instead of the console
|
||||
- `LOG_LEVEL=debug` will set the log level to debug instead of info
|
||||
|
||||
## Building
|
||||
|
||||
### Getting Started
|
||||
|
||||
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install)
|
||||
and run `flutter pub get` to fetch dependencies.
|
||||
|
||||
First you will need a valid [flutter sdk installation](https://flutter.dev/docs/get-started/install).
|
||||
You will probably want to disable Analytics on the Flutter Tool: `flutter config --no-analytics`
|
||||
|
||||
This project uses the flutter `stable` channel
|
||||
|
||||
Once flutter is set up, run `flutter pub get` from this project folder to fetch dependencies.
|
||||
|
||||
By default a development version is built, which loads profiles from `$CWTCH_HOME/dev/`. This is so that you can build
|
||||
and test development builds with alternative profiles while running a release/stable version of Cwtch uninterrupted.
|
||||
To build a release version and load normal profiles, use `build-release.sh X` instead of `flutter build X`
|
||||
|
||||
### Building on Linux (for Linux)
|
||||
|
||||
- run `fetch-libcwtch-go.sh`libCwtch-go to fetch a prebuild version of `libCwtch-go.so` go to `./linux`. Include `./linux` in `LD_LIBRARY_PATH`
|
||||
- run `fetch-tor.sh` and/or ensure that `tor` is in `$PATH`
|
||||
- run `flutter run -d linux`
|
||||
- copy `libCwtch-go.so` to `linux/`, or run `fetch-libcwtch-go.sh` to download it
|
||||
- set `LD_LIBRARY_PATH="$PWD/linux"`
|
||||
- copy a `tor` binary to `linux/` or run `fetch-tor.sh` to download one
|
||||
- run `flutter config --enable-linux-desktop` if you've never done so before
|
||||
- optional: launch cwtch-ui debug build by running `flutter run -d linux`
|
||||
- to build cwtch-ui, run `flutter build linux`
|
||||
- optional: launch cwtch-ui release build with `env LD_LIBRARY_PATH=linux ./build/linux/x64/release/bundle/cwtch`
|
||||
- to package the build, run `linux/package-release.sh`
|
||||
|
||||
### Building on Windows (for Windows)
|
||||
|
||||
- run `fetch-libcwtch-go.ps1` to fetch a prebuild version of `libCwtch.dll`
|
||||
- copy `libCwtch.dll` to `windows/`, or run `fetch-libcwtch-go.ps1` to download it
|
||||
- run `fetch-tor-win.ps1` to fetch Tor for windows
|
||||
- run `flutter run -d windows`
|
||||
- optional: launch cwtch-ui debug build by running `flutter run -d windows`
|
||||
- to build cwtch-ui, run `flutter build windows`
|
||||
- optional: to run the release build:
|
||||
- `cp windows/libCwtch.dll .`
|
||||
- `./build/windows/runner/Release/cwtch.exe`
|
||||
|
||||
### Building on Linux/Windows (for Android)
|
||||
|
||||
- Follow the steps above to fetch `libCwtch-go` and `tor` (these will fetch Android versions of these binaries also)
|
||||
- run `flutter run` with an Android phone connect via USB (or some other valid debug mode)
|
||||
|
||||
### Building on MacOS
|
||||
|
||||
- Cocaopods is required, you may need to `gem install cocaopods -v 1.9.3`
|
||||
- copy `libCwtch.dylib` into the root folder, or run `fetch-libcwtch-go-macos.sh` to download it
|
||||
- run `fetch-tor-macos.sh` to fetch Tor or Download and install Tor Browser and `cp -r /Applications/Tor\ Browser.app/Contents/MacOS/Tor ./macos/`
|
||||
- `flutter build macos`
|
||||
- optional: launch cwtch-ui release build with `./build/macos/Build/Products/Release/Cwtch.app/Contents/MacOS/Cwtch`
|
||||
- To package the UI: `./macos/package-release.sh`, which results in a Cwtch.dmg that has libCwtch.dylib and tor in it as well and can be installed into Applications
|
||||
|
||||
### Known Platform Issues
|
||||
|
||||
- **Windows**: Flutter engine has a [known bug](https://github.com/flutter/flutter/issues/75675) around the Right Shift key being sticky.
|
||||
|
|
|
@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
@ -48,7 +48,7 @@ android {
|
|||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "im.cwtch.flwtch"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
|
|
@ -48,4 +48,11 @@
|
|||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
@ -15,6 +15,10 @@ import cwtch.Cwtch
|
|||
import io.flutter.FlutterInjector
|
||||
import org.json.JSONObject
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import android.net.Uri
|
||||
|
||||
class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
||||
CoroutineWorker(context, parameters) {
|
||||
|
@ -56,6 +60,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
if (Cwtch.startCwtch(appDir, torPath) != 0.toLong()) return Result.failure()
|
||||
|
||||
Log.i("FlwtchWorker.kt", "startCwtch success, starting coroutine AppbusEvent loop...")
|
||||
val downloadIDs = mutableMapOf<String, Int>()
|
||||
while(true) {
|
||||
val evt = MainActivity.AppbusEvent(Cwtch.getAppBusEvent())
|
||||
if (evt.EventType == "NewMessageFromPeer" || evt.EventType == "NewMessageFromGroup") {
|
||||
|
@ -93,6 +98,65 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
.build()
|
||||
notificationManager.notify(getNotificationID(data.getString("ProfileOnion"), handle), newNotification)
|
||||
}
|
||||
} else if (evt.EventType == "FileDownloadProgressUpdate") {
|
||||
try {
|
||||
val data = JSONObject(evt.Data);
|
||||
val fileKey = data.getString("FileKey");
|
||||
val title = data.getString("NameSuggestion");
|
||||
val progress = data.getString("Progress").toInt();
|
||||
val progressMax = data.getString("FileSizeInChunks").toInt();
|
||||
if (!downloadIDs.containsKey(fileKey)) {
|
||||
downloadIDs.put(fileKey, downloadIDs.count());
|
||||
}
|
||||
var dlID = downloadIDs.get(fileKey);
|
||||
if (dlID == null) {
|
||||
dlID = 0;
|
||||
}
|
||||
if (progress >= 0) {
|
||||
val channelId =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createDownloadNotificationChannel(fileKey, fileKey)
|
||||
} 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 newNotification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setOngoing(true)
|
||||
.setContentTitle("Downloading")//todo: translate
|
||||
.setContentText(title)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setProgress(progressMax, progress, false)
|
||||
.setSound(null)
|
||||
//.setSilent(true)
|
||||
.build();
|
||||
notificationManager.notify(dlID, newNotification);
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.i("FlwtchWorker->FileDownloadProgressUpdate", e.toString() + " :: " + e.getStackTrace());
|
||||
}
|
||||
} else if (evt.EventType == "FileDownloaded") {
|
||||
Log.i("FlwtchWorker", "file downloaded!");
|
||||
val data = JSONObject(evt.Data);
|
||||
val tempFile = data.getString("TempFile");
|
||||
val fileKey = data.getString("FileKey");
|
||||
if (tempFile != "") {
|
||||
val filePath = data.getString("FilePath");
|
||||
Log.i("FlwtchWorker", "moving "+tempFile+" to "+filePath);
|
||||
val sourcePath = Paths.get(tempFile);
|
||||
val targetUri = Uri.parse(filePath);
|
||||
val os = this.applicationContext.getContentResolver().openOutputStream(targetUri);
|
||||
val bytesWritten = Files.copy(sourcePath, os);
|
||||
Log.i("FlwtchWorker", "copied " + bytesWritten.toString() + " bytes");
|
||||
if (bytesWritten != 0L) {
|
||||
os?.flush();
|
||||
os?.close();
|
||||
Files.delete(sourcePath);
|
||||
}
|
||||
}
|
||||
if (downloadIDs.containsKey(fileKey)) {
|
||||
notificationManager.cancel(downloadIDs.get(fileKey)?:0);
|
||||
}
|
||||
}
|
||||
|
||||
Intent().also { intent ->
|
||||
|
@ -157,6 +221,34 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
val target = (a.get("target") as? String) ?: ""
|
||||
Cwtch.sendInvitation(profile, handle, target)
|
||||
}
|
||||
"ShareFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
Cwtch.shareFile(profile, handle, filepath)
|
||||
}
|
||||
"DownloadFile" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
val filepath = (a.get("filepath") as? String) ?: ""
|
||||
val manifestpath = (a.get("manifestpath") as? String) ?: ""
|
||||
val filekey = (a.get("filekey") as? String) ?: ""
|
||||
// FIXME: Prevent spurious calls by Intent
|
||||
if (profile != "") {
|
||||
Cwtch.downloadFile(profile, handle, filepath, manifestpath, filekey)
|
||||
}
|
||||
}
|
||||
"CheckDownloadStatus" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.checkDownloadStatus(profile, fileKey)
|
||||
}
|
||||
"VerifyOrResumeDownload" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
val fileKey = (a.get("fileKey") as? String) ?: ""
|
||||
Cwtch.verifyOrResumeDownload(profile, handle, fileKey)
|
||||
}
|
||||
"SendProfileEvent" -> {
|
||||
val onion = (a.get("onion") as? String) ?: ""
|
||||
val jsonEvent = (a.get("jsonEvent") as? String) ?: ""
|
||||
|
@ -192,25 +284,76 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
val pass = (a.get("pass") as? String) ?: ""
|
||||
Cwtch.deleteProfile(profile, pass)
|
||||
}
|
||||
"LeaveConversation" -> {
|
||||
"ArchiveConversation" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val contactHandle = (a.get("contactHandle") as? String) ?: ""
|
||||
Cwtch.leaveConversation(profile, contactHandle)
|
||||
val contactHandle = (a.get("handle") as? String) ?: ""
|
||||
Cwtch.archiveConversation(profile, contactHandle)
|
||||
}
|
||||
"LeaveGroup" -> {
|
||||
"DeleteContact" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val groupHandle = (a.get("groupHandle") as? String) ?: ""
|
||||
Cwtch.leaveGroup(profile, groupHandle)
|
||||
val handle = (a.get("handle") as? String) ?: ""
|
||||
Cwtch.deleteContact(profile, handle)
|
||||
}
|
||||
"RejectInvite" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val groupHandle = (a.get("groupHandle") as? String) ?: ""
|
||||
Cwtch.rejectInvite(profile, groupHandle)
|
||||
}
|
||||
"SetProfileAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setProfileAttribute(profile, key, v)
|
||||
}
|
||||
"SetContactAttribute" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val contact = (a.get("Contact") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setContactAttribute(profile, contact, key, v)
|
||||
}
|
||||
"Shutdown" -> {
|
||||
Cwtch.shutdownCwtch();
|
||||
return Result.success()
|
||||
}
|
||||
"LoadServers" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.loadServers(password)
|
||||
}
|
||||
"CreateServer" -> {
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
val desc = (a.get("Description") as? String) ?: ""
|
||||
val autostart = (a.get("Autostart") as? Boolean) ?: false
|
||||
Cwtch.createServer(password, desc, autostart)
|
||||
}
|
||||
"DeleteServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val password = (a.get("Password") as? String) ?: ""
|
||||
Cwtch.deleteServer(serverOnion, password)
|
||||
}
|
||||
"LaunchServers" -> {
|
||||
Cwtch.launchServers()
|
||||
}
|
||||
"LaunchServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.launchServer(serverOnion)
|
||||
}
|
||||
"StopServer" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
Cwtch.stopServer(serverOnion)
|
||||
}
|
||||
"StopServers" -> {
|
||||
Cwtch.stopServers()
|
||||
}
|
||||
"DestroyServers" -> {
|
||||
Cwtch.destroyServers()
|
||||
}
|
||||
"SetServerAttribute" -> {
|
||||
val serverOnion = (a.get("ServerOnion") as? String) ?: ""
|
||||
val key = (a.get("Key") as? String) ?: ""
|
||||
val v = (a.get("Val") as? String) ?: ""
|
||||
Cwtch.setServerAttribute(serverOnion, key, v)
|
||||
}
|
||||
else -> return Result.failure()
|
||||
}
|
||||
return Result.success()
|
||||
|
@ -268,6 +411,15 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
return channelId
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createDownloadNotificationChannel(channelId: String, channelName: String): String{
|
||||
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)
|
||||
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"
|
||||
|
|
|
@ -12,17 +12,25 @@ import android.view.Window
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.*
|
||||
|
||||
import io.flutter.embedding.android.SplashScreen
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.ErrorLogResult
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.content.ContentUris
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.database.Cursor
|
||||
import android.provider.MediaStore
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
||||
|
||||
|
@ -47,6 +55,13 @@ class MainActivity: FlutterActivity() {
|
|||
private var notificationClickChannel: MethodChannel? = null
|
||||
private var shutdownClickChannel: MethodChannel? = null
|
||||
|
||||
// "Download to..." prompt extra arguments
|
||||
private val FILEPICKER_REQUEST_CODE = 234
|
||||
private var dlToProfile = ""
|
||||
private var dlToHandle = ""
|
||||
private var dlToFileKey = ""
|
||||
|
||||
// handles clicks received from outside the app (ie, notifications)
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
if (notificationClickChannel == null || intent.extras == null) return
|
||||
|
@ -68,6 +83,28 @@ class MainActivity: FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// handles return values from the system file picker
|
||||
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
|
||||
super.onActivityResult(requestCode, result, intent);
|
||||
|
||||
if (intent == null || intent!!.getData() == null) {
|
||||
Log.i("MainActivity:onActivityResult", "user canceled activity");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestCode == FILEPICKER_REQUEST_CODE) {
|
||||
val filePath = intent!!.getData().toString();
|
||||
val manifestPath = StringBuilder().append(this.applicationContext.cacheDir).append("/").append(this.dlToFileKey).toString();
|
||||
handleCwtch(MethodCall("DownloadFile", mapOf(
|
||||
"ProfileOnion" to this.dlToProfile,
|
||||
"handle" to this.dlToHandle,
|
||||
"filepath" to filePath,
|
||||
"manifestpath" to manifestPath,
|
||||
"filekey" to this.dlToFileKey
|
||||
)), ErrorLogResult(""));//placeholder; this Result is never actually invoked
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
// Note: this methods are invoked on the main thread.
|
||||
|
@ -125,6 +162,18 @@ class MainActivity: FlutterActivity() {
|
|||
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
|
||||
} else if (call.method == "CreateDownloadableFile") {
|
||||
this.dlToProfile = argmap["ProfileOnion"] ?: ""
|
||||
this.dlToHandle = argmap["handle"] ?: ""
|
||||
val suggestedName = argmap["filename"] ?: "filename.ext"
|
||||
this.dlToFileKey = argmap["filekey"] ?: ""
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/octet-stream"
|
||||
putExtra(Intent.EXTRA_TITLE, suggestedName)
|
||||
}
|
||||
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
|
||||
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
|
||||
|
@ -178,20 +227,6 @@ class MainActivity: FlutterActivity() {
|
|||
WorkManager.getInstance(this).pruneWork()
|
||||
}
|
||||
|
||||
// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095
|
||||
// for reference:
|
||||
//
|
||||
// class Response(json: String) : JSONObject(json) {
|
||||
// val type: String? = this.optString("type")
|
||||
// val data = this.optJSONArray("data")
|
||||
// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject
|
||||
// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo
|
||||
// }
|
||||
//
|
||||
// class Foo(json: String) : JSONObject(json) {
|
||||
// val id = this.optInt("id")
|
||||
// val title: String? = this.optString("title")
|
||||
// }
|
||||
class AppbusEvent(json: String) : JSONObject(json) {
|
||||
val EventType = this.optString("EventType")
|
||||
val EventID = this.optString("EventID")
|
||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.4'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "build-release.sh [android|linux|macos|windows]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "VERSION" ]; then
|
||||
VERSION=`cat VERSION`
|
||||
else
|
||||
VERSION=`git describe --tags --abbrev=1`
|
||||
fi
|
||||
|
||||
if [ -f "BUILDDATE" ]; then
|
||||
BUILDDATE=`cat BUILDDATE`
|
||||
else
|
||||
BUILDDATE=`date +%G-%m-%d-%H-%M`
|
||||
fi
|
||||
|
||||
flutter build $1 --dart-define BUILD_VER=$VERSION --dart-define BUILD_DATE=$BUILDDATE
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
VERSION=`cat LIBCWTCH-GO-MACOS.version`
|
||||
echo $VERSION
|
||||
|
||||
curl https://build.openprivacy.ca/files/libCwtch-go-macos-$VERSION/libCwtch.dylib --output libCwtch.dylib
|
|
@ -5,5 +5,3 @@ echo $VERSION
|
|||
|
||||
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/cwtch.aar -O android/cwtch/cwtch.aar
|
||||
wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.so -O linux/libCwtch.so
|
||||
|
||||
# wget https://build.openprivacy.ca/files/libCwtch-go-$VERSION/libCwtch.dll -O windows/libCwtch.dll
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd macos
|
||||
curl https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-macos-0.4.6.7.tar.gz --output tor.tar.gz
|
||||
tar -xzf tor.tar.gz
|
||||
chmod a+x Tor/tor.real
|
||||
cd ..
|
|
@ -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://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-win64-0.4.6.5.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 '7917561a7a063440a1ddfa9cb544ab9ffd09de84cea3dd66e3cc9cd349dd9f85b74a522ec390d7a974bc19b424c4d53af60e57bbc47e763d13cab6a203c4592f' ) { Write-Error 'tor.zip sha512sum mismatch' }
|
||||
|
||||
Expand-Archive -Path tor.zip -DestinationPath Tor
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import 'package:flutter/src/services/text_input.dart';
|
||||
|
||||
// To handle profiles that are "unencrypted" (i.e don't require a password to open) we currently create a profile with a defacto, hardcoded password.
|
||||
// Details: https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
|
||||
const DefaultPassword = "be gay do crime";
|
||||
|
||||
abstract class Cwtch {
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<void> Start();
|
||||
|
@ -41,12 +45,23 @@ abstract class Cwtch {
|
|||
void SendInvitation(String profile, String handle, String target);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void LeaveConversation(String profile, String handle);
|
||||
void ShareFile(String profile, String handle, String filepath);
|
||||
// ignore: non_constant_identifier_names
|
||||
void DownloadFile(String profile, String handle, String filepath, String manifestpath, String filekey);
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateDownloadableFile(String profile, String handle, String filenameSuggestion, String filekey);
|
||||
// ignore: non_constant_identifier_names
|
||||
void CheckDownloadStatus(String profile, String fileKey);
|
||||
// ignore: non_constant_identifier_names
|
||||
void VerifyOrResumeDownload(String profile, String handle, String filekey);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void ArchiveConversation(String profile, String handle);
|
||||
// ignore: non_constant_identifier_names
|
||||
void DeleteContact(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
|
||||
void ImportBundle(String profile, String bundle);
|
||||
|
@ -54,6 +69,29 @@ abstract class Cwtch {
|
|||
void SetGroupAttribute(String profile, String groupHandle, String key, String value);
|
||||
// ignore: non_constant_identifier_names
|
||||
void RejectInvite(String profileOnion, String groupHandle);
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetProfileAttribute(String profile, String key, String val);
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetContactAttribute(String profile, String contact, String key, String val);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void LoadServers(String password);
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateServer(String password, String description, bool autostart);
|
||||
// ignore: non_constant_identifier_names
|
||||
void DeleteServer(String serverOnion, String password);
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServers();
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServer(String serverOnion);
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServer(String serverOnion);
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServers();
|
||||
// ignore: non_constant_identifier_names
|
||||
void DestroyServers();
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetServerAttribute(String serverOnion, String key, String val);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void Shutdown();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'package:cwtch/models/message.dart';
|
||||
import 'package:cwtch/models/profileservers.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/notification_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -20,14 +21,16 @@ class CwtchNotifier {
|
|||
late TorStatus torStatus;
|
||||
late NotificationsManager notificationManager;
|
||||
late AppState appState;
|
||||
late ServerListState serverListState;
|
||||
|
||||
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN) {
|
||||
CwtchNotifier(ProfileListState pcn, Settings settingsCN, ErrorHandler errorCN, TorStatus torStatusCN, NotificationsManager notificationManagerP, AppState appStateCN, ServerListState serverListStateCN) {
|
||||
profileCN = pcn;
|
||||
settings = settingsCN;
|
||||
error = errorCN;
|
||||
torStatus = torStatusCN;
|
||||
notificationManager = notificationManagerP;
|
||||
appState = appStateCN;
|
||||
serverListState = serverListStateCN;
|
||||
}
|
||||
|
||||
void handleMessage(String type, dynamic data) {
|
||||
|
@ -55,14 +58,32 @@ class CwtchNotifier {
|
|||
numUnread: int.parse(data["unread"]),
|
||||
isGroup: data["isGroup"] == true,
|
||||
server: data["groupServer"],
|
||||
archived: data["isArchived"] == true,
|
||||
lastMessageTime: DateTime.now(), //show at the top of the contact list even if no messages yet
|
||||
));
|
||||
break;
|
||||
case "NewServer":
|
||||
EnvironmentConfig.debugLog("NewServer $data");
|
||||
serverListState.add(
|
||||
data["Onion"],
|
||||
data["ServerBundle"],
|
||||
data["Running"] == "true",
|
||||
data["Description"],
|
||||
data["Autostart"] == "true",
|
||||
data["StorageType"] == "storage-password");
|
||||
break;
|
||||
case "ServerIntentUpdate":
|
||||
EnvironmentConfig.debugLog("ServerIntentUpdate $data");
|
||||
var server = serverListState.getServer(data["Identity"]);
|
||||
if (server != null) {
|
||||
server.setRunning(data["Intent"] == "running");
|
||||
}
|
||||
break;
|
||||
case "GroupCreated":
|
||||
|
||||
// Retrieve Server Status from Cache...
|
||||
String status = "";
|
||||
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
|
||||
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])?.serverList.getServer(data["GroupServer"]);
|
||||
if (serverInfoState != null) {
|
||||
status = serverInfoState.status;
|
||||
}
|
||||
|
@ -83,6 +104,12 @@ class CwtchNotifier {
|
|||
// todo standarize
|
||||
error.handleUpdate("deleteprofile.success");
|
||||
break;
|
||||
case "ServerDeleted":
|
||||
error.handleUpdate("deletedserver." + data["Status"]);
|
||||
if (data["Status"] == "success") {
|
||||
serverListState.delete(data["Identity"]);
|
||||
}
|
||||
break;
|
||||
case "DeleteContact":
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.removeContact(data["RemotePeer"]);
|
||||
break;
|
||||
|
@ -106,6 +133,8 @@ class CwtchNotifier {
|
|||
notificationManager.notify("New Message From Peer!");
|
||||
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
|
||||
} else {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.newMarker++;
|
||||
}
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
|
||||
|
@ -144,24 +173,34 @@ class CwtchNotifier {
|
|||
break;
|
||||
case "NewMessageFromGroup":
|
||||
if (data["ProfileOnion"] != data["RemotePeer"]) {
|
||||
//if not currently open
|
||||
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
|
||||
var idx = int.parse(data["Index"]);
|
||||
var currentTotal = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages;
|
||||
|
||||
// Only bother to do anything if we know about the group and the provided index is greater than our current total...
|
||||
if (currentTotal != null && idx >= currentTotal) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages = idx + 1;
|
||||
|
||||
//if not currently open
|
||||
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
|
||||
} else {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.newMarker++;
|
||||
}
|
||||
|
||||
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
||||
// TODO: There are 2 timestamps associated with a new group message - time sent and time received.
|
||||
// Sent refers to the time a profile alleges they sent a message
|
||||
// Received refers to the time we actually saw the message from the server
|
||||
// These can obviously be very different for legitimate reasons.
|
||||
// We also maintain a relative hash-link through PreviousMessageSignature which is the ground truth for
|
||||
// order.
|
||||
// In the future we will want to combine these 3 ordering mechanisms into a cohesive view of the timeline
|
||||
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
|
||||
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
|
||||
// and `local now`.
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], timestampSent.toLocal());
|
||||
notificationManager.notify("New Message From Group!");
|
||||
}
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
|
||||
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
|
||||
// TODO: There are 2 timestamps associated with a new group message - time sent and time received.
|
||||
// Sent refers to the time a profile alleges they sent a message
|
||||
// Received refers to the time we actually saw the message from the server
|
||||
// These can obviously be very different for legitimate reasons.
|
||||
// We also maintain a relative hash-link through PreviousMessageSignature which is the ground truth for
|
||||
// order.
|
||||
// In the future we will want to combine these 3 ordering mechanisms into a cohesive view of the timeline
|
||||
// For now we perform some minimal checks on the sent timestamp to use to provide a useful ordering for honest contacts
|
||||
// and ensure that malicious contacts in groups can only set this timestamp to a value within the range of `last seen message time`
|
||||
// and `local now`.
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], timestampSent.toLocal());
|
||||
notificationManager.notify("New Message From Group!");
|
||||
} else {
|
||||
// from me (already displayed - do not update counter)
|
||||
var idx = data["Signature"];
|
||||
|
@ -179,10 +218,15 @@ class CwtchNotifier {
|
|||
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"]);
|
||||
var total = int.parse(data["Data"]);
|
||||
if (total != profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages) {
|
||||
profileCN.getProfile(data["Identity"])?.contactList.getContact(contactHandle)!.totalMessages = total;
|
||||
}
|
||||
break;
|
||||
case "SendMessageToPeerError":
|
||||
// Ignore
|
||||
break;
|
||||
case "IndexedFailure":
|
||||
EnvironmentConfig.debugLog("IndexedFailure");
|
||||
var idx = data["Index"];
|
||||
var key = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])?.getMessageKey(idx);
|
||||
try {
|
||||
|
@ -249,7 +293,7 @@ class CwtchNotifier {
|
|||
|
||||
// Retrieve Server Status from Cache...
|
||||
String status = "";
|
||||
ServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
|
||||
RemoteServerInfoState? serverInfoState = profileCN.getProfile(data["ProfileOnion"])!.serverList.getServer(groupInvite["ServerHost"]);
|
||||
if (serverInfoState != null) {
|
||||
status = serverInfoState.status;
|
||||
}
|
||||
|
@ -289,12 +333,31 @@ class CwtchNotifier {
|
|||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.nickname = data["Data"];
|
||||
}
|
||||
} else if (data["Key"] == "local.archived") {
|
||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) != null) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isArchived = data["Data"] == "true";
|
||||
}
|
||||
} else {
|
||||
EnvironmentConfig.debugLog("unhandled set group attribute event: ${data['Key']}");
|
||||
}
|
||||
break;
|
||||
case "SetPeerAttribute":
|
||||
if (data["Key"] == "local.name") {
|
||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.nickname = data["Data"];
|
||||
}
|
||||
} else if (data["Key"] == "local.archived") {
|
||||
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"]) != null) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.isArchived = data["Data"] == "true";
|
||||
}
|
||||
} else if (data["Key"] == "LastKnowSignature") {
|
||||
// group syncing information that isn't relevant to the UI...
|
||||
} else {
|
||||
EnvironmentConfig.debugLog("unhandled set peer attribute event: ${data['Key']}");
|
||||
}
|
||||
break;
|
||||
case "NewRetValMessageFromPeer":
|
||||
if (data["Path"] == "name") {
|
||||
if (data["Path"] == "name" && data["Data"].toString().trim().length > 0) {
|
||||
// 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"];
|
||||
|
@ -303,6 +366,20 @@ class CwtchNotifier {
|
|||
EnvironmentConfig.debugLog("unhandled peer attribute event: ${data['Path']}");
|
||||
}
|
||||
break;
|
||||
case "ManifestSaved":
|
||||
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkManifest(data["FileKey"]);
|
||||
break;
|
||||
case "FileDownloadProgressUpdate":
|
||||
var progress = int.parse(data["Progress"]);
|
||||
profileCN.getProfile(data["ProfileOnion"])?.downloadUpdate(data["FileKey"], progress, int.parse(data["FileSizeInChunks"]));
|
||||
// progress == -1 is a "download was interrupted" message and should contain a path
|
||||
if (progress < 0) {
|
||||
profileCN.getProfile(data["ProfileOnion"])?.downloadSetPath(data["FileKey"], data["FilePath"]);
|
||||
}
|
||||
break;
|
||||
case "FileDownloaded":
|
||||
profileCN.getProfile(data["ProfileOnion"])?.downloadMarkFinished(data["FileKey"], data["FilePath"]);
|
||||
break;
|
||||
default:
|
||||
EnvironmentConfig.debugLog("unhandled event: $type");
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:io';
|
|||
import 'dart:isolate';
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:cwtch/cwtch/cwtchNotifier.dart';
|
||||
import 'package:flutter/src/services/text_input.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
@ -12,6 +11,9 @@ import 'package:cwtch/cwtch/cwtch.dart';
|
|||
|
||||
import '../config.dart';
|
||||
|
||||
import "package:path/path.dart" show dirname, join;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
/////////////////////
|
||||
/// Cwtch API ///
|
||||
/////////////////////
|
||||
|
@ -22,6 +24,9 @@ typedef StartCwtchFn = int Function(Pointer<Utf8> dir, int len, Pointer<Utf8> to
|
|||
typedef void_from_void_funtion = Void Function();
|
||||
typedef VoidFromVoidFunction = void Function();
|
||||
|
||||
typedef free_function = Void Function(Pointer<Utf8>);
|
||||
typedef FreeFn = void Function(Pointer<Utf8>);
|
||||
|
||||
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
||||
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
|
@ -31,11 +36,14 @@ 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 VoidFromStringStringStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef void_from_string_string_string_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
||||
typedef VoidFromStringStringStringStringStringFn = void Function(Pointer<Utf8>, int, 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 NextEventFn = void Function();
|
||||
typedef void_from_string_string_byte_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int8);
|
||||
typedef VoidFromStringStringByteFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
|
||||
|
||||
typedef string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length);
|
||||
typedef StringFn = void Function(Pointer<Utf8> dir, int);
|
||||
|
@ -43,16 +51,9 @@ typedef StringFn = void Function(Pointer<Utf8> dir, int);
|
|||
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
|
||||
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
typedef get_json_blob_void_function = Pointer<Utf8> Function();
|
||||
typedef GetJsonBlobVoidFn = Pointer<Utf8> Function();
|
||||
|
||||
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
|
||||
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
|
||||
|
||||
//func NumMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) (n C.int) {
|
||||
typedef get_int_from_str_str_function = Int32 Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
|
||||
typedef GetIntFromStrStrFn = int Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
|
||||
|
||||
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
|
||||
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
|
||||
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
|
||||
|
@ -64,22 +65,35 @@ typedef GetJsonBlobFromStrStrStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
|
|||
typedef appbus_events_function = Pointer<Utf8> Function();
|
||||
typedef AppbusEventsFn = Pointer<Utf8> Function();
|
||||
|
||||
const String UNSUPPORTED_OS = "unsupported-os";
|
||||
|
||||
class CwtchFfi implements Cwtch {
|
||||
late DynamicLibrary library;
|
||||
late CwtchNotifier cwtchNotifier;
|
||||
late Isolate cwtchIsolate;
|
||||
ReceivePort _receivePort = ReceivePort();
|
||||
|
||||
CwtchFfi(CwtchNotifier _cwtchNotifier) {
|
||||
static String getLibraryPath() {
|
||||
if (Platform.isWindows) {
|
||||
library = DynamicLibrary.open("libCwtch.dll");
|
||||
return "libCwtch.dll";
|
||||
} else if (Platform.isLinux) {
|
||||
library = DynamicLibrary.open("libCwtch.so");
|
||||
return "libCwtch.so";
|
||||
} else if (Platform.isMacOS) {
|
||||
print(dirname(Platform.script.path));
|
||||
return "libCwtch.dylib";
|
||||
} else {
|
||||
return UNSUPPORTED_OS;
|
||||
}
|
||||
}
|
||||
|
||||
CwtchFfi(CwtchNotifier _cwtchNotifier) {
|
||||
String library_path = getLibraryPath();
|
||||
if (library_path == UNSUPPORTED_OS) {
|
||||
print("OS ${Platform.operatingSystem} not supported by cwtch/ffi");
|
||||
// emergency, ideally the app stays on splash and just posts the error till user closes
|
||||
exit(0);
|
||||
}
|
||||
library = DynamicLibrary.open(library_path);
|
||||
cwtchNotifier = _cwtchNotifier;
|
||||
}
|
||||
|
||||
|
@ -88,8 +102,9 @@ class CwtchFfi implements Cwtch {
|
|||
String home = "";
|
||||
String bundledTor = "";
|
||||
Map<String, String> envVars = Platform.environment;
|
||||
String cwtchDir = "";
|
||||
if (Platform.isLinux) {
|
||||
home = envVars['HOME']!;
|
||||
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, ".cwtch");
|
||||
if (await File("linux/tor").exists()) {
|
||||
bundledTor = "linux/tor";
|
||||
} else if (await File("lib/tor").exists()) {
|
||||
|
@ -102,14 +117,50 @@ class CwtchFfi implements Cwtch {
|
|||
bundledTor = "tor";
|
||||
}
|
||||
} else if (Platform.isWindows) {
|
||||
home = envVars['UserProfile']!;
|
||||
cwtchDir = envVars['CWTCH_DIR'] ?? path.join(envVars['UserProfile']!, ".cwtch");
|
||||
bundledTor = "Tor\\Tor\\tor.exe";
|
||||
} else if (Platform.isMacOS) {
|
||||
cwtchDir = envVars['CWTCH_HOME'] ?? path.join(envVars['HOME']!, "Library/Application Support/Cwtch");
|
||||
if (await File("Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
|
||||
bundledTor = "Cwtch.app/Contents/MacOS/Tor/tor.real";
|
||||
} else if (await File("/Applications/Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
|
||||
bundledTor = "/Applications/Cwtch.app/Contents/MacOS/Tor/tor.real";
|
||||
} else if (await File("/Volumes/Cwtch/Cwtch.app/Contents/MacOS/Tor/tor.real").exists()) {
|
||||
bundledTor = "/Volumes/Cwtch/Cwtch.app/Contents/MacOS/Tor/tor.real";
|
||||
} else if (await File("/Applications/Tor Browser.app/Contents/MacOS/Tor/tor.real").exists()) {
|
||||
bundledTor = "/Applications/Tor Browser.app/Contents/MacOS/Tor/tor.real";
|
||||
print("We couldn't find Tor in the Cwtch app directory, however we can fall back to the Tor Browser binary");
|
||||
} else {
|
||||
var splitPath = path.split(dirname(Platform.script.path));
|
||||
if (splitPath[0] == "/" && splitPath[1] == "Applications") {
|
||||
var appName = splitPath[2];
|
||||
print("We're running in /Applications in a non standard app name: $appName");
|
||||
if (await File("/Applications/$appName/Contents/MacOS/Tor/tor.real").exists()) {
|
||||
bundledTor = "/Applications/$appName/Contents/MacOS/Tor/tor.real";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the first Cwtch MacOS release (1.2) accidently was a dev build
|
||||
// we need to temporarily remedy this for a release or two then delete
|
||||
// if macOs and release build and no profile and is dev profile
|
||||
// copy dev profile to release profile
|
||||
if (Platform.isMacOS && EnvironmentConfig.BUILD_VER != dev_version) {
|
||||
var devProfileExists = await Directory(path.join(cwtchDir, "dev", "profiles")).exists();
|
||||
var releaseProfileExists = await Directory(path.join(cwtchDir, "profiles")).exists();
|
||||
if (devProfileExists && !releaseProfileExists) {
|
||||
print("MacOS one time dev -> release profile migration...");
|
||||
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "profiles"), cwtchDir]);
|
||||
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "SALT"), cwtchDir]);
|
||||
await Process.run("cp", ["-r", "-p", path.join(cwtchDir, "dev", "ui.globals"), cwtchDir]);
|
||||
}
|
||||
}
|
||||
|
||||
var cwtchDir = envVars['CWTCH_HOME'] ?? path.join(home, ".cwtch");
|
||||
if (EnvironmentConfig.BUILD_VER == dev_version) {
|
||||
cwtchDir = path.join(cwtchDir, "dev");
|
||||
}
|
||||
|
||||
print("StartCwtch( cwtchdir: $cwtchDir, torPath: $bundledTor )");
|
||||
|
||||
var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
|
||||
|
@ -138,9 +189,7 @@ class CwtchFfi implements Cwtch {
|
|||
// Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events
|
||||
@override
|
||||
void dispose() {
|
||||
if (cwtchIsolate != null) {
|
||||
cwtchIsolate.kill(priority: Isolate.immediate);
|
||||
}
|
||||
cwtchIsolate.kill(priority: Isolate.immediate);
|
||||
}
|
||||
|
||||
// Entry point for an isolate to listen to a stream of events pulled from libcwtch-go and return them on the sendPort
|
||||
|
@ -154,20 +203,27 @@ class CwtchFfi implements Cwtch {
|
|||
|
||||
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
|
||||
static Stream<String> pollAppbusEvents() async* {
|
||||
late DynamicLibrary library;
|
||||
if (Platform.isWindows) {
|
||||
library = DynamicLibrary.open("libCwtch.dll");
|
||||
} else if (Platform.isLinux) {
|
||||
library = DynamicLibrary.open("libCwtch.so");
|
||||
}
|
||||
late DynamicLibrary library = DynamicLibrary.open(getLibraryPath());
|
||||
|
||||
var getAppbusEventC = library.lookup<NativeFunction<appbus_events_function>>("c_GetAppBusEvent");
|
||||
// ignore: non_constant_identifier_names
|
||||
final GetAppbusEvent = getAppbusEventC.asFunction<AppbusEventsFn>();
|
||||
|
||||
while (true) {
|
||||
// Embedded Version of _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved
|
||||
var free = library.lookup<NativeFunction<free_function>>("c_FreePointer");
|
||||
final Free = free.asFunction<FreeFn>();
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
final GetAppBusEvent = () {
|
||||
// ignore: non_constant_identifier_names
|
||||
Pointer<Utf8> result = GetAppbusEvent();
|
||||
String event = result.toDartString();
|
||||
Free(result);
|
||||
return event;
|
||||
};
|
||||
|
||||
while (true) {
|
||||
final event = GetAppBusEvent();
|
||||
|
||||
if (event.startsWith("{\"EventType\":\"Shutdown\"")) {
|
||||
print("Shutting down isolate thread: $event");
|
||||
|
@ -184,6 +240,7 @@ class CwtchFfi implements Cwtch {
|
|||
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
|
||||
final ut8Onion = onion.toNativeUtf8();
|
||||
SelectProfile(ut8Onion, ut8Onion.length);
|
||||
malloc.free(ut8Onion);
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -194,6 +251,8 @@ class CwtchFfi implements Cwtch {
|
|||
final utf8nick = nick.toNativeUtf8();
|
||||
final ut8pass = pass.toNativeUtf8();
|
||||
CreateProfile(utf8nick, utf8nick.length, ut8pass, ut8pass.length);
|
||||
malloc.free(utf8nick);
|
||||
malloc.free(ut8pass);
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -203,6 +262,7 @@ class CwtchFfi implements Cwtch {
|
|||
final LoadProfiles = loadProfileC.asFunction<StringFn>();
|
||||
final ut8pass = pass.toNativeUtf8();
|
||||
LoadProfiles(ut8pass, ut8pass.length);
|
||||
malloc.free(ut8pass);
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -214,6 +274,9 @@ class CwtchFfi implements Cwtch {
|
|||
final utf8handle = handle.toNativeUtf8();
|
||||
Pointer<Utf8> jsonMessageBytes = GetMessage(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(utf8handle);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
|
@ -226,6 +289,8 @@ class CwtchFfi implements Cwtch {
|
|||
final utf8onion = onion.toNativeUtf8();
|
||||
final utf8json = json.toNativeUtf8();
|
||||
SendAppBusEvent(utf8onion, utf8onion.length, utf8json, utf8json.length);
|
||||
malloc.free(utf8onion);
|
||||
malloc.free(utf8json);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -236,6 +301,7 @@ class CwtchFfi implements Cwtch {
|
|||
final SendAppBusEvent = sendAppBusEvent.asFunction<StringFn>();
|
||||
final utf8json = json.toNativeUtf8();
|
||||
SendAppBusEvent(utf8json, utf8json.length);
|
||||
malloc.free(utf8json);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -247,6 +313,8 @@ class CwtchFfi implements Cwtch {
|
|||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = contactHandle.toNativeUtf8();
|
||||
AcceptContact(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -258,6 +326,8 @@ class CwtchFfi implements Cwtch {
|
|||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = contactHandle.toNativeUtf8();
|
||||
BlockContact(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -270,6 +340,9 @@ class CwtchFfi implements Cwtch {
|
|||
final u2 = contactHandle.toNativeUtf8();
|
||||
final u3 = message.toNativeUtf8();
|
||||
SendMessage(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -282,8 +355,80 @@ class CwtchFfi implements Cwtch {
|
|||
final u2 = contactHandle.toNativeUtf8();
|
||||
final u3 = target.toNativeUtf8();
|
||||
SendInvitation(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, String contactHandle, String filepath) {
|
||||
var shareFile = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_ShareFile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ShareFile = shareFile.asFunction<VoidFromStringStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = contactHandle.toNativeUtf8();
|
||||
final u3 = filepath.toNativeUtf8();
|
||||
ShareFile(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) {
|
||||
var dlFile = library.lookup<NativeFunction<void_from_string_string_string_string_string_function>>("c_DownloadFile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final DownloadFile = dlFile.asFunction<VoidFromStringStringStringStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = contactHandle.toNativeUtf8();
|
||||
final u3 = filepath.toNativeUtf8();
|
||||
final u4 = manifestpath.toNativeUtf8();
|
||||
final u5 = filekey.toNativeUtf8();
|
||||
DownloadFile(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length, u5, u5.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
malloc.free(u4);
|
||||
malloc.free(u5);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) {
|
||||
// android only - do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void CheckDownloadStatus(String profileOnion, String fileKey) {
|
||||
var checkDownloadStatus = library.lookup<NativeFunction<string_string_to_void_function>>("c_CheckDownloadStatus");
|
||||
// ignore: non_constant_identifier_names
|
||||
final CheckDownloadStatus = checkDownloadStatus.asFunction<VoidFromStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = fileKey.toNativeUtf8();
|
||||
CheckDownloadStatus(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) {
|
||||
var fn = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_VerifyOrResumeDownload");
|
||||
// ignore: non_constant_identifier_names
|
||||
final VerifyOrResumeDownload = fn.asFunction<VoidFromStringStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = contactHandle.toNativeUtf8();
|
||||
final u3 = filekey.toNativeUtf8();
|
||||
VerifyOrResumeDownload(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ResetTor() {
|
||||
|
@ -302,6 +447,8 @@ class CwtchFfi implements Cwtch {
|
|||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = bundle.toNativeUtf8();
|
||||
ImportBundle(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -315,6 +462,10 @@ class CwtchFfi implements Cwtch {
|
|||
final u3 = key.toNativeUtf8();
|
||||
final u4 = value.toNativeUtf8();
|
||||
SetGroupAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
malloc.free(u4);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -326,9 +477,12 @@ class CwtchFfi implements Cwtch {
|
|||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = groupHandle.toNativeUtf8();
|
||||
RejectInvite(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
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
|
||||
|
@ -337,31 +491,40 @@ class CwtchFfi implements Cwtch {
|
|||
final u2 = server.toNativeUtf8();
|
||||
final u3 = groupName.toNativeUtf8();
|
||||
CreateGroup(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LeaveConversation(String profileOnion, String handle) {
|
||||
var leaveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveConversation");
|
||||
void ArchiveConversation(String profileOnion, String handle) {
|
||||
var archiveConversation = library.lookup<NativeFunction<string_string_to_void_function>>("c_ArchiveConversation");
|
||||
// ignore: non_constant_identifier_names
|
||||
final LeaveConversation = leaveConversation.asFunction<VoidFromStringStringFn>();
|
||||
final ArchiveConversation = archiveConversation.asFunction<VoidFromStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = handle.toNativeUtf8();
|
||||
LeaveConversation(u1, u1.length, u2, u2.length);
|
||||
ArchiveConversation(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LeaveGroup(String profileOnion, String groupHandle) {
|
||||
var leaveGroup = library.lookup<NativeFunction<string_string_to_void_function>>("c_LeaveGroup");
|
||||
void DeleteContact(String profileOnion, String handle) {
|
||||
var deleteContact = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteContact");
|
||||
// ignore: non_constant_identifier_names
|
||||
final LeaveGroup = leaveGroup.asFunction<VoidFromStringStringFn>();
|
||||
final DeleteContact = deleteContact.asFunction<VoidFromStringStringFn>();
|
||||
final u1 = profileOnion.toNativeUtf8();
|
||||
final u2 = groupHandle.toNativeUtf8();
|
||||
LeaveGroup(u1, u1.length, u2, u2.length);
|
||||
final u2 = handle.toNativeUtf8();
|
||||
DeleteContact(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
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
|
||||
|
@ -369,6 +532,8 @@ class CwtchFfi implements Cwtch {
|
|||
final utf8profile = profile.toNativeUtf8();
|
||||
final utf8handle = handle.toNativeUtf8();
|
||||
updateMessageFlags(utf8profile, utf8profile.length, utf8handle, utf8handle.length, index, flags);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(utf8handle);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -380,14 +545,151 @@ class CwtchFfi implements Cwtch {
|
|||
final u1 = onion.toNativeUtf8();
|
||||
final u2 = currentPassword.toNativeUtf8();
|
||||
DeleteProfile(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetProfileAttribute(String profile, String key, String val) {
|
||||
var setProfileAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetProfileAttribute");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SetProfileAttribute = setProfileAttribute.asFunction<VoidFromStringStringStringFn>();
|
||||
final u1 = profile.toNativeUtf8();
|
||||
final u2 = key.toNativeUtf8();
|
||||
final u3 = key.toNativeUtf8();
|
||||
SetProfileAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetContactAttribute(String profile, String contact, String key, String val) {
|
||||
var setContactAttribute = library.lookup<NativeFunction<void_from_string_string_string_string_function>>("c_SetContactAttribute");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SetContactAttribute = setContactAttribute.asFunction<VoidFromStringStringStringStringFn>();
|
||||
final u1 = profile.toNativeUtf8();
|
||||
final u2 = contact.toNativeUtf8();
|
||||
final u3 = key.toNativeUtf8();
|
||||
final u4 = key.toNativeUtf8();
|
||||
SetContactAttribute(u1, u1.length, u2, u2.length, u3, u3.length, u4, u4.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
malloc.free(u4);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LoadServers(String password) {
|
||||
var loadServers = library.lookup<NativeFunction<string_to_void_function>>("c_LoadServers");
|
||||
// ignore: non_constant_identifier_names
|
||||
final LoadServers = loadServers.asFunction<StringFn>();
|
||||
final u1 = password.toNativeUtf8();
|
||||
LoadServers(u1, u1.length);
|
||||
malloc.free(u1);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateServer(String password, String description, bool autostart) {
|
||||
var createServer = library.lookup<NativeFunction<void_from_string_string_byte_function>>("c_CreateServer");
|
||||
// ignore: non_constant_identifier_names
|
||||
final CreateServer = createServer.asFunction<VoidFromStringStringByteFn>();
|
||||
final u1 = password.toNativeUtf8();
|
||||
final u2 = description.toNativeUtf8();
|
||||
CreateServer(u1, u1.length, u2, u2.length, autostart ? 1 : 0);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DeleteServer(String serverOnion, String password) {
|
||||
var deleteServer = library.lookup<NativeFunction<string_string_to_void_function>>("c_DeleteServer");
|
||||
// ignore: non_constant_identifier_names
|
||||
final DeleteServer = deleteServer.asFunction<VoidFromStringStringFn>();
|
||||
final u1 = serverOnion.toNativeUtf8();
|
||||
final u2 = password.toNativeUtf8();
|
||||
DeleteServer(u1, u1.length, u2, u2.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServers() {
|
||||
var launchServers = library.lookup<NativeFunction<Void Function()>>("c_LaunchServers");
|
||||
// ignore: non_constant_identifier_names
|
||||
final LaunchServers = launchServers.asFunction<void Function()>();
|
||||
LaunchServers();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServer(String serverOnion) {
|
||||
var launchServer = library.lookup<NativeFunction<string_to_void_function>>("c_LaunchServer");
|
||||
// ignore: non_constant_identifier_names
|
||||
final LaunchServer = launchServer.asFunction<StringFn>();
|
||||
final u1 = serverOnion.toNativeUtf8();
|
||||
LaunchServer(u1, u1.length);
|
||||
malloc.free(u1);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServer(String serverOnion) {
|
||||
var shutdownServer = library.lookup<NativeFunction<string_to_void_function>>("c_StopServer");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ShutdownServer = shutdownServer.asFunction<StringFn>();
|
||||
final u1 = serverOnion.toNativeUtf8();
|
||||
ShutdownServer(u1, u1.length);
|
||||
malloc.free(u1);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServers() {
|
||||
var shutdownServers = library.lookup<NativeFunction<Void Function()>>("c_StopServers");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ShutdownServers = shutdownServers.asFunction<void Function()>();
|
||||
ShutdownServers();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DestroyServers() {
|
||||
var destroyServers = library.lookup<NativeFunction<Void Function()>>("c_DestroyServers");
|
||||
// ignore: non_constant_identifier_names
|
||||
final DestroyServers = destroyServers.asFunction<void Function()>();
|
||||
DestroyServers();
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetServerAttribute(String serverOnion, String key, String val) {
|
||||
var setServerAttribute = library.lookup<NativeFunction<void_from_string_string_string_function>>("c_SetServerAttribute");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SetServerAttribute = setServerAttribute.asFunction<VoidFromStringStringStringFn>();
|
||||
final u1 = serverOnion.toNativeUtf8();
|
||||
final u2 = key.toNativeUtf8();
|
||||
final u3 = val.toNativeUtf8();
|
||||
SetServerAttribute(u1, u1.length, u2, u2.length, u3, u3.length);
|
||||
malloc.free(u1);
|
||||
malloc.free(u2);
|
||||
malloc.free(u3);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
Future<void> Shutdown() async {
|
||||
var shutdown = library.lookup<NativeFunction<void_from_void_funtion>>("c_ShutdownCwtch");
|
||||
// ignore: non_constant_identifier_names
|
||||
|
||||
// Shutdown Cwtch + Tor...
|
||||
// ignore: non_constant_identifier_names
|
||||
final Shutdown = shutdown.asFunction<VoidFromVoidFunction>();
|
||||
Shutdown();
|
||||
|
||||
|
@ -400,6 +702,7 @@ class CwtchFfi implements Cwtch {
|
|||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
Future GetMessageByContentHash(String profile, String handle, String contentHash) async {
|
||||
var getMessagesByContentHashC = library.lookup<NativeFunction<get_json_blob_from_str_str_str_function>>("c_GetMessagesByContentHash");
|
||||
// ignore: non_constant_identifier_names
|
||||
|
@ -409,6 +712,20 @@ class CwtchFfi implements Cwtch {
|
|||
final utf8contentHash = contentHash.toNativeUtf8();
|
||||
Pointer<Utf8> jsonMessageBytes = GetMessagesByContentHash(utf8profile, utf8profile.length, utf8handle, utf8handle.length, utf8contentHash, utf8contentHash.length);
|
||||
String jsonMessage = jsonMessageBytes.toDartString();
|
||||
|
||||
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
|
||||
malloc.free(utf8profile);
|
||||
malloc.free(utf8handle);
|
||||
malloc.free(utf8contentHash);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
// Incredibly dangerous function which invokes a free in libCwtch, should only be used
|
||||
// as documented in `MEMORY.md` in libCwtch repo.
|
||||
void _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(Pointer<Utf8> ptr) {
|
||||
var free = library.lookup<NativeFunction<free_function>>("c_FreePointer");
|
||||
final Free = free.asFunction<FreeFn>();
|
||||
Free(ptr);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,6 +131,35 @@ class CwtchGomobile implements Cwtch {
|
|||
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "handle": contactHandle, "target": target});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, String contactHandle, String filepath) {
|
||||
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DownloadFile(String profileOnion, String contactHandle, String filepath, String manifestpath, String filekey) {
|
||||
cwtchPlatform.invokeMethod("DownloadFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filepath": filepath, "manifestpath": manifestpath, "filekey": filekey});
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateDownloadableFile(String profileOnion, String contactHandle, String filenameSuggestion, String filekey) {
|
||||
cwtchPlatform.invokeMethod("CreateDownloadableFile", {"ProfileOnion": profileOnion, "handle": contactHandle, "filename": filenameSuggestion, "filekey": filekey});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void CheckDownloadStatus(String profileOnion, String fileKey) {
|
||||
cwtchPlatform.invokeMethod("CheckDownloadStatus", {"ProfileOnion": profileOnion, "fileKey": fileKey});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void VerifyOrResumeDownload(String profileOnion, String contactHandle, String filekey) {
|
||||
cwtchPlatform.invokeMethod("VerifyOrResumeDownload", {"ProfileOnion": profileOnion, "handle": contactHandle, "filekey": filekey});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ResetTor() {
|
||||
|
@ -162,23 +191,89 @@ class CwtchGomobile implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LeaveGroup(String profileOnion, String groupHandle) {
|
||||
cwtchPlatform.invokeMethod("LeaveGroup", {"ProfileOnion": profileOnion, "groupHandle": groupHandle});
|
||||
void DeleteContact(String profileOnion, String handle) {
|
||||
cwtchPlatform.invokeMethod("DeleteContact", {"ProfileOnion": profileOnion, "handle": handle});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LeaveConversation(String profileOnion, String contactHandle) {
|
||||
cwtchPlatform.invokeMethod("LeaveConversation", {"ProfileOnion": profileOnion, "contactHandle": contactHandle});
|
||||
void ArchiveConversation(String profileOnion, String contactHandle) {
|
||||
cwtchPlatform.invokeMethod("ArchiveConversation", {"ProfileOnion": profileOnion, "handle": 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});
|
||||
cwtchPlatform.invokeMethod("UpdateMessageFlags", {"profile": profile, "contact": handle, "midx": index, "flags": flags});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetProfileAttribute(String profile, String key, String val) {
|
||||
cwtchPlatform.invokeMethod("SetProfileAttribute", {"ProfileOnion": profile, "Key": key, "Val": val});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetContactAttribute(String profile, String contact, String key, String val) {
|
||||
cwtchPlatform.invokeMethod("SetContactAttribute", {"ProfileOnion": profile, "Contact": contact, "Key": key, "Val": val});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LoadServers(String password) {
|
||||
cwtchPlatform.invokeMethod("LoadServers", {"Password": password});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void CreateServer(String password, String description, bool autostart) {
|
||||
cwtchPlatform.invokeMethod("CreateServer", {"Password": password, "Description": description, "Autostart": autostart});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DeleteServer(String serverOnion, String password) {
|
||||
cwtchPlatform.invokeMethod("DeleteServer", {"ServerOnion": serverOnion, "Password": password});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServers() {
|
||||
cwtchPlatform.invokeMethod("LaunchServers", {});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void LaunchServer(String serverOnion) {
|
||||
cwtchPlatform.invokeMethod("LaunchServer", {"ServerOnion": serverOnion});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServer(String serverOnion) {
|
||||
cwtchPlatform.invokeMethod("StopServer", {"ServerOnion": serverOnion});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void StopServers() {
|
||||
cwtchPlatform.invokeMethod("StopServers", {});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void DestroyServers() {
|
||||
cwtchPlatform.invokeMethod("DestroyServers", {});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SetServerAttribute(String serverOnion, String key, String val) {
|
||||
cwtchPlatform.invokeMethod("SetServerAttribute", {"ServerOnion": serverOnion, "Key": key, "Val": val});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> Shutdown() async {
|
||||
print("gomobile.dart Shutdown");
|
||||
cwtchPlatform.invokeMethod("Shutdown", {});
|
||||
|
|
|
@ -104,6 +104,16 @@ class CwtchIcons {
|
|||
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 streamer_bunnymask = IconData(0xe85b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData streamer_ghost = IconData(0xe85c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData cancel_schedule_send_black_24dp = IconData(0xe85d, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData more_horiz_black_24dp = IconData(0xe85e, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_add_24dp = IconData(0xe85f, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData dns_black_24dp = IconData(0xe860, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData info_black_24dp = IconData(0xe861, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData accept_unknown = IconData(0xe862, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_closed_1 = IconData(0xe863, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData eye_open_1 = IconData(0xe864, 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);
|
||||
|
|
|
@ -21,6 +21,27 @@ class ErrorHandler extends ChangeNotifier {
|
|||
bool deleteProfileError = false;
|
||||
bool deleteProfileSuccess = false;
|
||||
|
||||
static const String deletedServerErrorPrefix = "deletedserver";
|
||||
bool deletedServerError = false;
|
||||
bool deletedServerSuccess = false;
|
||||
|
||||
reset() {
|
||||
invalidImportStringError = false;
|
||||
contactAlreadyExistsError = false;
|
||||
explicitAddContactSuccess = false;
|
||||
|
||||
importBundleError = false;
|
||||
importBundleSuccess = false;
|
||||
|
||||
deleteProfileError = false;
|
||||
deleteProfileSuccess = false;
|
||||
|
||||
deletedServerError = false;
|
||||
deletedServerSuccess = false;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Called by the event bus.
|
||||
handleUpdate(String error) {
|
||||
var parts = error.split(".");
|
||||
|
@ -37,6 +58,8 @@ class ErrorHandler extends ChangeNotifier {
|
|||
case deleteProfileErrorPrefix:
|
||||
handleDeleteProfileError(errorType);
|
||||
break;
|
||||
case deletedServerErrorPrefix:
|
||||
handleDeletedServerError(errorType);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
|
@ -91,4 +114,19 @@ class ErrorHandler extends ChangeNotifier {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleDeletedServerError(String errorType) {
|
||||
// reset
|
||||
deletedServerError = false;
|
||||
deletedServerSuccess = false;
|
||||
|
||||
switch (errorType) {
|
||||
case successErrorType:
|
||||
deletedServerSuccess = true;
|
||||
break;
|
||||
default:
|
||||
deletedServerError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "de",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
"fileInterrupted": "Interrupted",
|
||||
"fileSavedTo": "Saved to",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Really delete server",
|
||||
"deleteServerSuccess": "Successfully deleted server",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Copy Address",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profiles",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
|
||||
"settingFileSharing": "File Sharing",
|
||||
"tooltipSendFile": "Send File",
|
||||
"messageFileOffered": "Contact is offering to send you a file",
|
||||
"messageFileSent": "You sent a file",
|
||||
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
|
||||
"labelFilesize": "Size",
|
||||
"labelFilename": "Filename",
|
||||
"downloadFileButton": "Herunterladen",
|
||||
"openFolderButton": "Open Folder",
|
||||
"retrievingManifestMessage": "Retrieving file information...",
|
||||
"streamerModeLabel": "Streamer\/Presentation Mode",
|
||||
"archiveConversation": "Archive this Conversation",
|
||||
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
|
||||
"addPeerTab": "Einen anderen Nutzer hinzufügen",
|
||||
"addPeer": "Anderen Nutzer hinzufügen",
|
||||
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
|
||||
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
|
||||
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
|
||||
"blockBtn": "Anderen Nutzer blockieren",
|
||||
"savePeerHistory": "Peer-Verlauf speichern",
|
||||
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
|
||||
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
|
||||
"unblockBtn": "Anderen Nutzer entsperren",
|
||||
"blockUnknownLabel": "Unbekannte Peers blockieren",
|
||||
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
|
||||
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "noch zu erledigen",
|
||||
"newConnectionPaneTitle": "Neue Verbindung",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Verbinde zu Netzwerk und Peers ...",
|
||||
"networkStatusAttemptingTor": "Versuche, eine Verbindung mit dem Tor-Netzwerk herzustellen",
|
||||
"networkStatusDisconnected": "Vom Internet getrennt, überprüfe deine Verbindung",
|
||||
"viewGroupMembershipTooltip": "Gruppenmitgliedschaft anzeigen",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Sprache",
|
||||
"blockUnknownLabel": "Unbekannte Peers blockieren",
|
||||
"zoomLabel": "Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen)",
|
||||
"versionBuilddate": "Version: %1 Aufgebaut auf: %2",
|
||||
"cwtchSettingsTitle": "Cwtch Einstellungen",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Passwort",
|
||||
"currentPasswordLabel": "aktuelles Passwort",
|
||||
"yourDisplayName": "Dein Anzeigename",
|
||||
"profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
|
||||
"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)",
|
||||
"radioUsePassword": "Passwort",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Profil bearbeiten",
|
||||
"addProfileTitle": "Neues Profil hinzufügen",
|
||||
"deleteBtn": "Löschen",
|
||||
"unblockBtn": "Anderen Nutzer entsperren",
|
||||
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
|
||||
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
|
||||
"savePeerHistory": "Peer-Verlauf speichern",
|
||||
"blockBtn": "Anderen Nutzer blockieren",
|
||||
"saveBtn": "Speichern",
|
||||
"displayNameLabel": "Angezeigename",
|
||||
"addressLabel": "Adresse",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Möchtest Du die Einladung annehmen",
|
||||
"newGroupBtn": "Neue Gruppe anlegen",
|
||||
"copiedClipboardNotification": "in die Zwischenablage kopiert",
|
||||
"peerOfflineMessage": "Anderer Nutzer ist offline, Nachrichten können derzeit nicht zugestellt werden",
|
||||
"peerBlockedMessage": "Anderer Nutzer ist blockiert",
|
||||
"pendingLabel": "Bestätigung ausstehend",
|
||||
"acknowledgedLabel": "bestätigt",
|
||||
"couldNotSendMsgError": "Nachricht konnte nicht gesendet werden",
|
||||
"dmTooltip": "Klicken, um Direktnachricht zu senden",
|
||||
"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.",
|
||||
"addListItemBtn": "Element hinzufügen",
|
||||
"peerNotOnline": "Der andere Nutzer ist offline. Die App kann momentan nicht verwendet werden.",
|
||||
"searchList": "Liste durchsuchen",
|
||||
"update": "Update",
|
||||
"inviteBtn": "Einladen",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "Neue Meldung",
|
||||
"joinGroup": "Gruppe beitreten",
|
||||
"createGroup": "Gruppe erstellen",
|
||||
"addPeer": "Anderen Nutzer hinzufügen",
|
||||
"groupAddr": "Adresse",
|
||||
"invitation": "Einladung",
|
||||
"server": "Server",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Adresse",
|
||||
"joinGroupTab": "Einer Gruppe beitreten",
|
||||
"createGroupTab": "Eine Gruppe erstellen",
|
||||
"addPeerTab": "Einen anderen Nutzer hinzufügen",
|
||||
"createGroupBtn": "Anlegen",
|
||||
"defaultGroupName": "Tolle Gruppe",
|
||||
"createGroupTitle": "Gruppe Anlegen"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "en",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
"fileInterrupted": "Interrupted",
|
||||
"fileSavedTo": "Saved to",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Really delete server",
|
||||
"deleteServerSuccess": "Successfully deleted server",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Copy Address",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profiles",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
|
||||
"settingFileSharing": "File Sharing",
|
||||
"tooltipSendFile": "Send File",
|
||||
"messageFileOffered": "Contact is offering to send you a file",
|
||||
"messageFileSent": "You sent a file",
|
||||
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
|
||||
"labelFilesize": "Size",
|
||||
"labelFilename": "Filename",
|
||||
"downloadFileButton": "Download",
|
||||
"openFolderButton": "Open Folder",
|
||||
"retrievingManifestMessage": "Retrieving file information...",
|
||||
"streamerModeLabel": "Streamer\/Presentation Mode",
|
||||
"archiveConversation": "Archive this Conversation",
|
||||
"profileOnionLabel": "Send this address to people you want to connect with",
|
||||
"addPeerTab": "Add a contact",
|
||||
"addPeer": "Add Contact",
|
||||
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
|
||||
"peerBlockedMessage": "Contact is blocked",
|
||||
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
|
||||
"blockBtn": "Block Contact",
|
||||
"savePeerHistory": "Save History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
|
||||
"dontSavePeerHistory": "Delete History",
|
||||
"unblockBtn": "Unblock Contact",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
|
||||
"networkStatusConnecting": "Connecting to network and contacts...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "Todo...",
|
||||
"newConnectionPaneTitle": "New Connection",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Connecting to network and peers...",
|
||||
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
|
||||
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
|
||||
"viewGroupMembershipTooltip": "View Group Membership",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Peers",
|
||||
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
|
||||
"versionBuilddate": "Version: %1 Built on: %2",
|
||||
"cwtchSettingsTitle": "Cwtch Settings",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"yourDisplayName": "Your Display Name",
|
||||
"profileOnionLabel": "Send this address to peers you want to connect with",
|
||||
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
|
||||
"radioNoPassword": "Unencrypted (No password)",
|
||||
"radioUsePassword": "Password",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Edit Profile",
|
||||
"addProfileTitle": "Add new profile",
|
||||
"deleteBtn": "Delete",
|
||||
"unblockBtn": "Unblock Peer",
|
||||
"dontSavePeerHistory": "Delete Peer History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
|
||||
"savePeerHistory": "Save Peer History",
|
||||
"blockBtn": "Block Peer",
|
||||
"saveBtn": "Save",
|
||||
"displayNameLabel": "Display Name",
|
||||
"addressLabel": "Address",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
|
||||
"newGroupBtn": "Create new group",
|
||||
"copiedClipboardNotification": "Copied to clipboard",
|
||||
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
|
||||
"peerBlockedMessage": "Peer is blocked",
|
||||
"pendingLabel": "Pending",
|
||||
"acknowledgedLabel": "Acknowledged",
|
||||
"couldNotSendMsgError": "Could not send this message",
|
||||
"dmTooltip": "Click to DM",
|
||||
"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.",
|
||||
"addListItemBtn": "Add Item",
|
||||
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
|
||||
"searchList": "Search List",
|
||||
"update": "Update",
|
||||
"inviteBtn": "Invite",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "New Bulletin",
|
||||
"joinGroup": "Join group",
|
||||
"createGroup": "Create group",
|
||||
"addPeer": "Add Peer",
|
||||
"groupAddr": "Address",
|
||||
"invitation": "Invitation",
|
||||
"server": "Server",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Address",
|
||||
"joinGroupTab": "Join a group",
|
||||
"createGroupTab": "Create a group",
|
||||
"addPeerTab": "Add a peer",
|
||||
"createGroupBtn": "Create",
|
||||
"defaultGroupName": "Awesome Group",
|
||||
"createGroupTitle": "Create Group"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "es",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
"fileInterrupted": "Interrupted",
|
||||
"fileSavedTo": "Saved to",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Really delete server",
|
||||
"deleteServerSuccess": "Successfully deleted server",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Copy Address",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profiles",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
|
||||
"settingFileSharing": "File Sharing",
|
||||
"tooltipSendFile": "Send File",
|
||||
"messageFileOffered": "Contact is offering to send you a file",
|
||||
"messageFileSent": "You sent a file",
|
||||
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
|
||||
"labelFilesize": "Size",
|
||||
"labelFilename": "Filename",
|
||||
"downloadFileButton": "Download",
|
||||
"openFolderButton": "Open Folder",
|
||||
"retrievingManifestMessage": "Retrieving file information...",
|
||||
"streamerModeLabel": "Streamer\/Presentation Mode",
|
||||
"archiveConversation": "Archive this Conversation",
|
||||
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
|
||||
"addPeerTab": "Agregar Contacto",
|
||||
"addPeer": "Agregar Contacto",
|
||||
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
|
||||
"peerBlockedMessage": "Contacto bloqueado",
|
||||
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
|
||||
"blockBtn": "Bloquear contacto",
|
||||
"savePeerHistory": "Guardar el historial con contacto",
|
||||
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
|
||||
"dontSavePeerHistory": "Eliminar historial de contacto",
|
||||
"unblockBtn": "Desbloquear contacto",
|
||||
"blockUnknownLabel": "Bloquear conexiones desconocidas",
|
||||
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
|
||||
"networkStatusConnecting": "Conectando a la red y a los contactos...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "Por hacer...",
|
||||
"newConnectionPaneTitle": "Nueva conexión",
|
||||
"networkStatusOnline": "En línea",
|
||||
"networkStatusConnecting": "Conectando a la red y a los contactos...",
|
||||
"networkStatusAttemptingTor": "Intentando conectarse a la red Tor",
|
||||
"networkStatusDisconnected": "Sin conexión, comprueba tu conexión",
|
||||
"viewGroupMembershipTooltip": "Ver membresía del grupo",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Francés",
|
||||
"localeEn": "Inglés",
|
||||
"settingLanguage": "Idioma",
|
||||
"blockUnknownLabel": "Bloquear conexiones desconocidas",
|
||||
"zoomLabel": "Zoom de la interfaz (afecta principalmente el tamaño del texto y de los botones)",
|
||||
"versionBuilddate": "Versión: %1 Basado en %2",
|
||||
"cwtchSettingsTitle": "Configuración de Cwtch",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Contraseña",
|
||||
"currentPasswordLabel": "Contraseña actual",
|
||||
"yourDisplayName": "Tu nombre de usuario",
|
||||
"profileOnionLabel": "Envía esta dirección a los contactos con los que quieras conectarte",
|
||||
"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)",
|
||||
"radioUsePassword": "Contraseña",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Editar perfil",
|
||||
"addProfileTitle": "Agregar nuevo perfil",
|
||||
"deleteBtn": "Eliminar",
|
||||
"unblockBtn": "Desbloquear contacto",
|
||||
"dontSavePeerHistory": "Eliminar historial de contacto",
|
||||
"savePeerHistoryDescription": "Determina si eliminar o no el historial asociado con el contacto.",
|
||||
"savePeerHistory": "Guardar el historial con contacto",
|
||||
"blockBtn": "Bloquear contacto",
|
||||
"saveBtn": "Guardar",
|
||||
"displayNameLabel": "Nombre de Usuario",
|
||||
"addressLabel": "Dirección",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "¿Quieres aceptar la invitación a ",
|
||||
"newGroupBtn": "Crear un nuevo grupo de chat",
|
||||
"copiedClipboardNotification": "Copiado al portapapeles",
|
||||
"peerOfflineMessage": "Este contacto no está en línea, los mensajes no pueden ser entregados en este momento",
|
||||
"peerBlockedMessage": "Contacto bloqueado",
|
||||
"pendingLabel": "Pendiente",
|
||||
"acknowledgedLabel": "Reconocido",
|
||||
"couldNotSendMsgError": "No se pudo enviar este mensaje",
|
||||
"dmTooltip": "Haz clic para enviar mensaje directo",
|
||||
"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",
|
||||
"addListItemBtn": "Agregar artículo",
|
||||
"peerNotOnline": "Este contacto no está en línea, la aplicación no puede ser usada en este momento",
|
||||
"searchList": "Buscar en la lista",
|
||||
"update": "Actualizar",
|
||||
"inviteBtn": "Invitar",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "Nuevo Boletín",
|
||||
"joinGroup": "Únete al grupo",
|
||||
"createGroup": "Crear perfil",
|
||||
"addPeer": "Agregar Contacto",
|
||||
"groupAddr": "Dirección",
|
||||
"invitation": "Invitación",
|
||||
"server": "Servidor",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"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"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "fr",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copier les clés",
|
||||
"verfiyResumeButton": "Vérifier\/reprendre",
|
||||
"fileCheckingStatus": "Vérification de l'état du téléchargement",
|
||||
"fileInterrupted": "Interrompu",
|
||||
"fileSavedTo": "Enregistré dans",
|
||||
"plainServerDescription": "Nous vous recommandons de protéger vos serveurs Cwtch par un mot de passe. Si vous ne définissez pas de mot de passe sur ce serveur, toute personne ayant accès à cet appareil peut être en mesure d'accéder aux informations concernant ce serveur, y compris les clés cryptographiques sensibles.",
|
||||
"encryptedServerDescription": "Le chiffrement d’un serveur avec un mot de passe le protège des autres personnes qui peuvent également utiliser cet appareil. Les serveurs cryptés ne peuvent pas être déchiffrés, affichés ou accessibles tant que le mot de passe correct n’est pas entré pour les déverrouiller.",
|
||||
"deleteServerConfirmBtn": "Supprimer vraiment le serveur",
|
||||
"deleteServerSuccess": "Le serveur a été supprimé avec succès",
|
||||
"enterCurrentPasswordForDeleteServer": "Veuillez saisir le mot de passe actuel pour supprimer ce serveur",
|
||||
"copyAddress": "Copier l'adresse",
|
||||
"settingServersDescription": "L'expérience des serveurs d'hébergement permet d'héberger et de gérer les serveurs Cwtch.",
|
||||
"settingServers": "Serveurs d'hébergement",
|
||||
"enterServerPassword": "Entrez le mot de passe pour déverrouiller le serveur",
|
||||
"unlockProfileTip": "Veuillez créer ou déverrouiller un profil pour commencer !",
|
||||
"unlockServerTip": "Veuillez créer ou déverrouiller un serveur pour commencer !",
|
||||
"addServerTooltip": "Ajouter un nouveau serveur",
|
||||
"serversManagerTitleShort": "Serveurs",
|
||||
"serversManagerTitleLong": "Serveurs que vous hébergez",
|
||||
"saveServerButton": "Enregistrer le serveur",
|
||||
"serverAutostartDescription": "Contrôle si l'application lance automatiquement le serveur au démarrage.",
|
||||
"serverAutostartLabel": "Démarrage automatique",
|
||||
"serverEnabledDescription": "Démarrer ou arrêter le serveur",
|
||||
"serverEnabled": "Serveur activé",
|
||||
"serverDescriptionDescription": "Votre description du serveur est à des fins de gestion personnelle uniquement, elle ne sera jamais partagée.",
|
||||
"serverDescriptionLabel": "Description du serveur",
|
||||
"serverAddress": "Adresse du serveur",
|
||||
"editServerTitle": "Modifier le serveur",
|
||||
"addServerTitle": "Ajouter un serveur",
|
||||
"titleManageProfilesShort": "Profils",
|
||||
"descriptionStreamerMode": "Si elle est activée, cette option donne un rendu visuel plus privé à l'application pour la diffusion en direct ou la présentation, par exemple, en masquant profil et adresses de contacts.",
|
||||
"descriptionFileSharing": "L'expérience de partage de fichiers vous permet d'envoyer et de recevoir des fichiers à partir de contacts et de groupes Cwtch. Notez que si vous partagez un fichier avec un groupe, les membres de ce groupe se connecteront avec vous directement via Cwtch pour le télécharger.",
|
||||
"settingFileSharing": "Partage de fichiers",
|
||||
"tooltipSendFile": "Envoyer le fichier",
|
||||
"messageFileOffered": "Contact vous propose de vous envoyer un fichier",
|
||||
"messageFileSent": "Vous avez envoyé un fichier",
|
||||
"messageEnableFileSharing": "Activez l'expérience de partage de fichiers pour afficher ce message.",
|
||||
"labelFilesize": "Taille",
|
||||
"labelFilename": "Nom de fichier",
|
||||
"downloadFileButton": "Télécharger",
|
||||
"openFolderButton": "Ouvrir le dossier",
|
||||
"retrievingManifestMessage": "Récupération des informations sur le fichier...",
|
||||
"streamerModeLabel": "Mode Streamer\/Présentation",
|
||||
"archiveConversation": "Archiver cette conversation",
|
||||
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
|
||||
"addPeerTab": "Ajouter un contact",
|
||||
"addPeer": "Ajouter le contact",
|
||||
"peerNotOnline": "Le contact est hors ligne. Les applications ne peuvent pas être utilisées pour le moment.",
|
||||
"peerBlockedMessage": "Le contact est bloqué",
|
||||
"peerOfflineMessage": "Le contact est hors ligne, les messages ne peuvent pas être transmis pour le moment.",
|
||||
"blockBtn": "Bloquer le contact",
|
||||
"savePeerHistory": "Enregistrer l'historique",
|
||||
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au contact.",
|
||||
"dontSavePeerHistory": "Supprimer l'historique",
|
||||
"unblockBtn": "Débloquer le contact",
|
||||
"blockUnknownLabel": "Bloquer les pairs inconnus",
|
||||
"blockUnknownConnectionsEnabledDescription": "Les connexions provenant de contacts inconnus sont bloquées. Vous pouvez modifier cela dans les paramètres",
|
||||
"networkStatusConnecting": "Connexion au réseau et aux contacts...",
|
||||
"showMessageButton": "Afficher le message",
|
||||
"blockedMessageMessage": "Ce message provient d'un profil que vous avez bloqué.",
|
||||
"placeholderEnterMessage": "saisissez un message",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "À faire...",
|
||||
"newConnectionPaneTitle": "Nouvelle connexion",
|
||||
"networkStatusOnline": "En ligne",
|
||||
"networkStatusConnecting": "Se connecter au réseau et aux pairs...",
|
||||
"networkStatusAttemptingTor": "Tentative de connexion au réseau Tor",
|
||||
"networkStatusDisconnected": "Déconnecté d'Internet, vérifiez votre connexion",
|
||||
"viewGroupMembershipTooltip": "Afficher les membres du groupe",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Français",
|
||||
"localeEn": "Anglais",
|
||||
"settingLanguage": "Langue",
|
||||
"blockUnknownLabel": "Bloquer les pairs inconnus",
|
||||
"zoomLabel": "Zoom de l'interface (affecte principalement la taille du texte et des boutons)",
|
||||
"versionBuilddate": "Version : %1 Construite le : %2",
|
||||
"cwtchSettingsTitle": "Préférences Cwtch",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Mot de passe",
|
||||
"currentPasswordLabel": "Mot de passe actuel",
|
||||
"yourDisplayName": "Pseudo",
|
||||
"profileOnionLabel": "Envoyez cette adresse aux personnes avec lesquelles vous souhaitez entrer en contact.",
|
||||
"noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.",
|
||||
"radioNoPassword": "Non chiffré (pas de mot de passe)",
|
||||
"radioUsePassword": "Mot de passe",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Modifier le profil",
|
||||
"addProfileTitle": "Ajouter un nouveau profil",
|
||||
"deleteBtn": "Effacer",
|
||||
"unblockBtn": "Débloquer le pair",
|
||||
"dontSavePeerHistory": "Supprimer l'historique des pairs",
|
||||
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au pair.",
|
||||
"savePeerHistory": "Sauvegarder l'historique des pairs",
|
||||
"blockBtn": "Bloquer le pair",
|
||||
"saveBtn": "Sauvegarder",
|
||||
"displayNameLabel": "Pseudo",
|
||||
"addressLabel": "Adresse",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Voulez-vous accepter l'invitation au groupe",
|
||||
"newGroupBtn": "Créer un nouveau groupe",
|
||||
"copiedClipboardNotification": "Copié dans le presse-papier",
|
||||
"peerOfflineMessage": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment",
|
||||
"peerBlockedMessage": "Le pair est bloqué",
|
||||
"pendingLabel": "En attente",
|
||||
"acknowledgedLabel": "Accusé de réception",
|
||||
"couldNotSendMsgError": "Impossible d'envoyer ce message",
|
||||
"dmTooltip": "Envoyer un message privé",
|
||||
"membershipDescription": "Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être représentatives de l'ensemble des membres du groupe.",
|
||||
"addListItemBtn": "Ajouter un élément",
|
||||
"peerNotOnline": "Le pair est hors ligne, les messages ne peuvent pas être remis pour le moment",
|
||||
"searchList": "Liste de recherche",
|
||||
"update": "Mise à jour",
|
||||
"inviteBtn": "Invitation",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "Nouveau bulletin",
|
||||
"joinGroup": "Rejoindre le groupe",
|
||||
"createGroup": "Créer un groupe",
|
||||
"addPeer": "Ajouter un pair",
|
||||
"groupAddr": "Adresse",
|
||||
"invitation": "Invitation",
|
||||
"server": "Serveur",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Adresse",
|
||||
"joinGroupTab": "Rejoindre un groupe",
|
||||
"createGroupTab": "Créer un groupe",
|
||||
"addPeerTab": "Ajouter un pair",
|
||||
"createGroupBtn": "Créer",
|
||||
"defaultGroupName": "Un groupe génial",
|
||||
"createGroupTitle": "Créer un groupe"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "it",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
"fileInterrupted": "Interrupted",
|
||||
"fileSavedTo": "Saved to",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Really delete server",
|
||||
"deleteServerSuccess": "Successfully deleted server",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Copy Address",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profiles",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "L'esperimento di condivisione dei file ti consente di inviare e ricevere file dai contatti e dai gruppi di Cwtch. Tieni presente che la condivisione di un file con un gruppo farà sì che i membri di quel gruppo si colleghino con te direttamente su Cwtch per scaricarlo.",
|
||||
"settingFileSharing": "Condivisione file",
|
||||
"tooltipSendFile": "Invia file",
|
||||
"messageFileOffered": "Il contatto offre l'invio di un file",
|
||||
"messageFileSent": "You sent a file",
|
||||
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
|
||||
"labelFilesize": "Size",
|
||||
"labelFilename": "Filename",
|
||||
"downloadFileButton": "Download",
|
||||
"openFolderButton": "Open Folder",
|
||||
"retrievingManifestMessage": "Retrieving file information...",
|
||||
"streamerModeLabel": "Streamer\/Presentation Mode",
|
||||
"archiveConversation": "Archive this Conversation",
|
||||
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
|
||||
"addPeerTab": "Aggiungi un peer",
|
||||
"addPeer": "Aggiungi peer",
|
||||
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
|
||||
"peerBlockedMessage": "Il peer è bloccato",
|
||||
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
|
||||
"blockBtn": "Blocca il peer",
|
||||
"savePeerHistory": "Salva cronologia peer",
|
||||
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
|
||||
"dontSavePeerHistory": "Elimina cronologia dei peer",
|
||||
"unblockBtn": "Sblocca il peer",
|
||||
"blockUnknownLabel": "Blocca peer sconosciuti",
|
||||
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
|
||||
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "Da fare...",
|
||||
"newConnectionPaneTitle": "Nuova connessione",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Connessione alla rete e ai peer ...",
|
||||
"networkStatusAttemptingTor": "Tentativo di connessione alla rete Tor",
|
||||
"networkStatusDisconnected": "Disconnesso da Internet, controlla la tua connessione",
|
||||
"viewGroupMembershipTooltip": "Visualizza i membri del gruppo",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Francese",
|
||||
"localeEn": "Inglese",
|
||||
"settingLanguage": "Lingua",
|
||||
"blockUnknownLabel": "Blocca peer sconosciuti",
|
||||
"zoomLabel": "Zoom dell'interfaccia (influisce principalmente sulle dimensioni del testo e dei pulsanti)",
|
||||
"versionBuilddate": "Versione: %1 Costruito il: %2",
|
||||
"cwtchSettingsTitle": "Impostazioni di Cwtch",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Password",
|
||||
"currentPasswordLabel": "Password corrente",
|
||||
"yourDisplayName": "Il tuo nome visualizzato",
|
||||
"profileOnionLabel": "Inviare questo indirizzo ai peer con cui si desidera connettersi",
|
||||
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
|
||||
"radioNoPassword": "Non criptato (senza password)",
|
||||
"radioUsePassword": "Password",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Modifica profilo",
|
||||
"addProfileTitle": "Aggiungi nuovo profilo",
|
||||
"deleteBtn": "Elimina",
|
||||
"unblockBtn": "Sblocca il peer",
|
||||
"dontSavePeerHistory": "Elimina cronologia dei peer",
|
||||
"savePeerHistoryDescription": "Determina se eliminare o meno ogni cronologia eventualmente associata al peer.",
|
||||
"savePeerHistory": "Salva cronologia peer",
|
||||
"blockBtn": "Blocca il peer",
|
||||
"saveBtn": "Salva",
|
||||
"displayNameLabel": "Nome visualizzato",
|
||||
"addressLabel": "Indirizzo",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Vuoi accettare l'invito a",
|
||||
"newGroupBtn": "Crea un nuovo gruppo",
|
||||
"copiedClipboardNotification": "Copiato negli Appunti",
|
||||
"peerOfflineMessage": "Il peer è offline, i messaggi non possono essere recapitati in questo momento",
|
||||
"peerBlockedMessage": "Il peer è bloccato",
|
||||
"pendingLabel": "In corso",
|
||||
"acknowledgedLabel": "Riconosciuto",
|
||||
"couldNotSendMsgError": "Impossibile inviare questo messaggio",
|
||||
"dmTooltip": "Clicca per inviare un Messagio Diretto",
|
||||
"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.",
|
||||
"addListItemBtn": "Aggiungi elemento",
|
||||
"peerNotOnline": "Il peer è offline. Le applicazioni non possono essere utilizzate in questo momento.",
|
||||
"searchList": "Cerca nella lista",
|
||||
"update": "Aggiornamento",
|
||||
"inviteBtn": "Invitare",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "Nuovo bollettino",
|
||||
"joinGroup": "Unisciti al gruppo",
|
||||
"createGroup": "Crea un gruppo",
|
||||
"addPeer": "Aggiungi peer",
|
||||
"groupAddr": "Indirizzo",
|
||||
"invitation": "Invito",
|
||||
"server": "Server",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Indirizzo",
|
||||
"joinGroupTab": "Unisciti a un gruppo",
|
||||
"createGroupTab": "Crea un gruppo",
|
||||
"addPeerTab": "Aggiungi un peer",
|
||||
"createGroupBtn": "Crea",
|
||||
"defaultGroupName": "Gruppo fantastico",
|
||||
"createGroupTitle": "Crea un gruppo"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "pl",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Kopiuj klucze",
|
||||
"verfiyResumeButton": "Zweryfikuj\/wznów",
|
||||
"fileCheckingStatus": "Sprawdzanie stanu pobierania",
|
||||
"fileInterrupted": "Przerwane",
|
||||
"fileSavedTo": "Zapisano do",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Naprawdę usuń serwer",
|
||||
"deleteServerSuccess": "Pomyślnie usunięto serwer",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Skopiuj adres",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profile",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "Eksperyment udostępniania plików pozwala na wysyłanie i odbieranie plików od kontaktów i grup Cwtch. Zauważ, że udostępnienie pliku grupie spowoduje, że członkowie tej grupy połączą się z Tobą bezpośrednio przez Cwtch, aby go pobrać.",
|
||||
"settingFileSharing": "Udostępnianie plików",
|
||||
"tooltipSendFile": "Wyślij plik",
|
||||
"messageFileOffered": "Kontakt proponuje wysłanie Ci pliku",
|
||||
"messageFileSent": "Plik został wysłany",
|
||||
"messageEnableFileSharing": "Włącz eksperyment udostępniania plików, aby wyświetlić tę wiadomość.",
|
||||
"labelFilesize": "Rozmiar",
|
||||
"labelFilename": "Nazwa pliku",
|
||||
"downloadFileButton": "Pobierz",
|
||||
"openFolderButton": "Otwórz folder",
|
||||
"retrievingManifestMessage": "Pobieranie informacji o pliku...",
|
||||
"streamerModeLabel": "Tryb streamera\/prezentacji",
|
||||
"archiveConversation": "Zarchiwizuj tę rozmowę",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"addPeerTab": "Add a contact",
|
||||
"addPeer": "Add Contact",
|
||||
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
|
||||
"peerBlockedMessage": "Contact is blocked",
|
||||
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
|
||||
"blockBtn": "Block Contact",
|
||||
"savePeerHistory": "Save History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
|
||||
"dontSavePeerHistory": "Delete History",
|
||||
"unblockBtn": "Unblock Contact",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"blockUnknownConnectionsEnabledDescription": "Połączenia od nieznanych kontaktów są blokowane. Można to zmienić w Ustawieniach",
|
||||
"networkStatusConnecting": "Connecting to network and contacts...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "Todo...",
|
||||
"newConnectionPaneTitle": "New Connection",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Connecting to network and peers...",
|
||||
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
|
||||
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
|
||||
"viewGroupMembershipTooltip": "View Group Membership",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Peers",
|
||||
"zoomLabel": "Interface zoom (mostly affects text and button sizes)",
|
||||
"versionBuilddate": "Version: %1 Built on: %2",
|
||||
"cwtchSettingsTitle": "Cwtch Settings",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"yourDisplayName": "Your Display Name",
|
||||
"profileOnionLabel": "Send this address to peers you want to connect with",
|
||||
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
|
||||
"radioNoPassword": "Unencrypted (No password)",
|
||||
"radioUsePassword": "Password",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Edit Profile",
|
||||
"addProfileTitle": "Add new profile",
|
||||
"deleteBtn": "Delete",
|
||||
"unblockBtn": "Unblock Peer",
|
||||
"dontSavePeerHistory": "Delete Peer History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
|
||||
"savePeerHistory": "Save Peer History",
|
||||
"blockBtn": "Block Peer",
|
||||
"saveBtn": "Save",
|
||||
"displayNameLabel": "Display Name",
|
||||
"addressLabel": "Address",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Do you want to accept the invitation to",
|
||||
"newGroupBtn": "Create new group",
|
||||
"copiedClipboardNotification": "Copied to clipboard",
|
||||
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
|
||||
"peerBlockedMessage": "Peer is blocked",
|
||||
"pendingLabel": "Pending",
|
||||
"acknowledgedLabel": "Acknowledged",
|
||||
"couldNotSendMsgError": "Could not send this message",
|
||||
"dmTooltip": "Click to DM",
|
||||
"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.",
|
||||
"addListItemBtn": "Add Item",
|
||||
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
|
||||
"searchList": "Search List",
|
||||
"update": "Update",
|
||||
"inviteBtn": "Invite",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "New Bulletin",
|
||||
"joinGroup": "Join group",
|
||||
"createGroup": "Create group",
|
||||
"addPeer": "Add Peer",
|
||||
"groupAddr": "Address",
|
||||
"invitation": "Invitation",
|
||||
"server": "Server",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Address",
|
||||
"joinGroupTab": "Join a group",
|
||||
"createGroupTab": "Create a group",
|
||||
"addPeerTab": "Add a peer",
|
||||
"createGroupBtn": "Create",
|
||||
"defaultGroupName": "Awesome Group",
|
||||
"createGroupTitle": "Create Group"
|
||||
|
|
|
@ -1,6 +1,66 @@
|
|||
{
|
||||
"@@locale": "pt",
|
||||
"@@last_modified": "2021-07-14T23:49:07+02:00",
|
||||
"@@last_modified": "2021-11-11T01:02:08+01:00",
|
||||
"newMessagesLabel": "New Messages",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Copy keys",
|
||||
"verfiyResumeButton": "Verify\/resume",
|
||||
"fileCheckingStatus": "Checking download status",
|
||||
"fileInterrupted": "Interrupted",
|
||||
"fileSavedTo": "Saved to",
|
||||
"plainServerDescription": "We recommend that you protect your Cwtch servers with a password. If you do not set a password on this server then anyone who has access to this device may be able to access information about this server, including sensitive cryptographic keys.",
|
||||
"encryptedServerDescription": "Encrypting a server with a password protects it from other people who may also use this device. Encrypted servers cannot be decrypted, displayed or accessed until the correct password is entered to unlock them.",
|
||||
"deleteServerConfirmBtn": "Really delete server",
|
||||
"deleteServerSuccess": "Successfully deleted server",
|
||||
"enterCurrentPasswordForDeleteServer": "Please enter current password to delete this server",
|
||||
"copyAddress": "Copy Address",
|
||||
"settingServersDescription": "The hosting servers experiment enables hosting and managing Cwtch servers",
|
||||
"settingServers": "Hosting Servers",
|
||||
"enterServerPassword": "Enter password to unlock server",
|
||||
"unlockProfileTip": "Please create or unlock a profile to begin!",
|
||||
"unlockServerTip": "Please create or unlock a server to begin!",
|
||||
"addServerTooltip": "Add new server",
|
||||
"serversManagerTitleShort": "Servers",
|
||||
"serversManagerTitleLong": "Servers You Host",
|
||||
"saveServerButton": "Save Server",
|
||||
"serverAutostartDescription": "Controls if the application will automatically launch the server on start",
|
||||
"serverAutostartLabel": "Autostart",
|
||||
"serverEnabledDescription": "Start or stop the server",
|
||||
"serverEnabled": "Server Enabled",
|
||||
"serverDescriptionDescription": "Your description of the server for personal management use only, will never be shared",
|
||||
"serverDescriptionLabel": "Server Description",
|
||||
"serverAddress": "Server Address",
|
||||
"editServerTitle": "Edit Server",
|
||||
"addServerTitle": "Add Server",
|
||||
"titleManageProfilesShort": "Profiles",
|
||||
"descriptionStreamerMode": "If turned on, this option makes the app more visually private for streaming or presenting with, for example, hiding profile and contact addresses",
|
||||
"descriptionFileSharing": "The file sharing experiment allows you to send and receive files from Cwtch contacts and groups. Note that sharing a file with a group will result in members of that group connecting with you directly over Cwtch to download it.",
|
||||
"settingFileSharing": "File Sharing",
|
||||
"tooltipSendFile": "Send File",
|
||||
"messageFileOffered": "Contact is offering to send you a file",
|
||||
"messageFileSent": "You sent a file",
|
||||
"messageEnableFileSharing": "Enable the file sharing experiment to view this message.",
|
||||
"labelFilesize": "Size",
|
||||
"labelFilename": "Filename",
|
||||
"downloadFileButton": "Download",
|
||||
"openFolderButton": "Open Folder",
|
||||
"retrievingManifestMessage": "Retrieving file information...",
|
||||
"streamerModeLabel": "Streamer\/Presentation Mode",
|
||||
"archiveConversation": "Archive this Conversation",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"addPeerTab": "Add a contact",
|
||||
"addPeer": "Add Contact",
|
||||
"peerNotOnline": "Contact is offline. Applications cannot be used right now.",
|
||||
"peerBlockedMessage": "Contact is blocked",
|
||||
"peerOfflineMessage": "Contact is offline, messages can't be delivered right now",
|
||||
"blockBtn": "Block Contact",
|
||||
"savePeerHistory": "Save History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the contact.",
|
||||
"dontSavePeerHistory": "Delete History",
|
||||
"unblockBtn": "Unblock Contact",
|
||||
"blockUnknownLabel": "Block Unknown Contacts",
|
||||
"blockUnknownConnectionsEnabledDescription": "Connections from unknown contacts are blocked. You can change this in Settings",
|
||||
"networkStatusConnecting": "Connecting to network and contacts...",
|
||||
"showMessageButton": "Show Message",
|
||||
"blockedMessageMessage": "This message is from a profile you have blocked.",
|
||||
"placeholderEnterMessage": "Type a message...",
|
||||
|
@ -84,7 +144,6 @@
|
|||
"todoPlaceholder": "Afazer…",
|
||||
"newConnectionPaneTitle": "New Connection",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusConnecting": "Connecting to network and peers...",
|
||||
"networkStatusAttemptingTor": "Attempting to connect to Tor network",
|
||||
"networkStatusDisconnected": "Disconnected from the internet, check your connection",
|
||||
"viewGroupMembershipTooltip": "View Group Membership",
|
||||
|
@ -104,7 +163,6 @@
|
|||
"localeFr": "Frances",
|
||||
"localeEn": "English",
|
||||
"settingLanguage": "Language",
|
||||
"blockUnknownLabel": "Block Unknown Peers",
|
||||
"zoomLabel": "Zoom da interface (afeta principalmente tamanho de texto e botões)",
|
||||
"versionBuilddate": "Version: %1 Built on: %2",
|
||||
"cwtchSettingsTitle": "Configurações do Cwtch",
|
||||
|
@ -128,7 +186,6 @@
|
|||
"password1Label": "Password",
|
||||
"currentPasswordLabel": "Current Password",
|
||||
"yourDisplayName": "Your Display Name",
|
||||
"profileOnionLabel": "Send this address to peers you want to connect with",
|
||||
"noPasswordWarning": "Not using a password on this account means that all data stored locally will not be encrypted",
|
||||
"radioNoPassword": "Unencrypted (No password)",
|
||||
"radioUsePassword": "Password",
|
||||
|
@ -141,11 +198,6 @@
|
|||
"editProfileTitle": "Edit Profile",
|
||||
"addProfileTitle": "Add new profile",
|
||||
"deleteBtn": "Deletar",
|
||||
"unblockBtn": "Unblock Peer",
|
||||
"dontSavePeerHistory": "Delete Peer History",
|
||||
"savePeerHistoryDescription": "Determines whether or not to delete any history associated with the peer.",
|
||||
"savePeerHistory": "Save Peer History",
|
||||
"blockBtn": "Block Peer",
|
||||
"saveBtn": "Salvar",
|
||||
"displayNameLabel": "Nome de Exibição",
|
||||
"addressLabel": "Endereço",
|
||||
|
@ -158,15 +210,12 @@
|
|||
"acceptGroupInviteLabel": "Você quer aceitar o convite para",
|
||||
"newGroupBtn": "Criar novo grupo",
|
||||
"copiedClipboardNotification": "Copiado",
|
||||
"peerOfflineMessage": "Peer is offline, messages can't be delivered right now",
|
||||
"peerBlockedMessage": "Peer is blocked",
|
||||
"pendingLabel": "Pendente",
|
||||
"acknowledgedLabel": "Confirmada",
|
||||
"couldNotSendMsgError": "Não deu para enviar esta mensagem",
|
||||
"dmTooltip": "Clique para DM",
|
||||
"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.",
|
||||
"addListItemBtn": "Add Item",
|
||||
"peerNotOnline": "Peer is Offline. Applications cannot be used right now.",
|
||||
"searchList": "Search List",
|
||||
"update": "Update",
|
||||
"inviteBtn": "Convidar",
|
||||
|
@ -192,7 +241,6 @@
|
|||
"newBulletinLabel": "Novo Boletim",
|
||||
"joinGroup": "Join group",
|
||||
"createGroup": "Create group",
|
||||
"addPeer": "Add Peer",
|
||||
"groupAddr": "Address",
|
||||
"invitation": "Invitation",
|
||||
"server": "Server",
|
||||
|
@ -201,7 +249,6 @@
|
|||
"peerAddress": "Address",
|
||||
"joinGroupTab": "Join a group",
|
||||
"createGroupTab": "Create a group",
|
||||
"addPeerTab": "Add a peer",
|
||||
"createGroupBtn": "Criar",
|
||||
"defaultGroupName": "Grupo incrível",
|
||||
"createGroupTitle": "Criar Grupo"
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
{
|
||||
"@@locale": "ru",
|
||||
"@@last_modified": "2021-11-10T18:47:30+01:00",
|
||||
"localeRU": "Russian",
|
||||
"copyServerKeys": "Копировать ключи",
|
||||
"verfiyResumeButton": "Проверить\/продолжить",
|
||||
"fileCheckingStatus": "Проверка статуса загрузки",
|
||||
"fileInterrupted": "Прервано",
|
||||
"fileSavedTo": "Сохранить в",
|
||||
"plainServerDescription": "Мы настоятельно рекомендуем защитить свой сервер Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к серверу, сможет получить доступ к информации на этом сервере включая конфиденциальные криптографические ключи.",
|
||||
"encryptedServerDescription": "Шифрование сервера паролем защитит его от других людей у которых может оказаться доступ к этому устройству, включая Onion адрес сервера. Зашифрованный сервер нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
|
||||
"deleteServerConfirmBtn": "Точно удалить сервер?",
|
||||
"deleteServerSuccess": "Сервер успешно удален",
|
||||
"enterCurrentPasswordForDeleteServer": "Пожалуйста, введите пароль сервера, чтобы удалить его",
|
||||
"copyAddress": "Копировать адрес",
|
||||
"settingServersDescription": "Экспериментальная функция которая позволяет добавлять сервер Cwtch. В меню появится дополнительная опция Серверы",
|
||||
"settingServers": "Использовать серверы",
|
||||
"enterServerPassword": "Введите пароль для разблокировки сервера",
|
||||
"unlockProfileTip": "Создайте или разблокируйте профиль, чтобы начать",
|
||||
"unlockServerTip": "Создайте или разблокируйте сервер, чтобы начать",
|
||||
"addServerTooltip": "Добавить сервер",
|
||||
"serversManagerTitleShort": "Серверы",
|
||||
"serversManagerTitleLong": "Личные серверы",
|
||||
"saveServerButton": "Сохранить сервер",
|
||||
"serverAutostartDescription": "Автозапуск сервера при старте программы",
|
||||
"serverAutostartLabel": "Автозапуск",
|
||||
"serverEnabledDescription": "Запустить или остановить сервер",
|
||||
"serverEnabled": "Сервер запущен",
|
||||
"serverDescriptionDescription": "Описание видите только Вы. Сделано для удобства",
|
||||
"serverDescriptionLabel": "Описание сервера",
|
||||
"serverAddress": "Адрес сервера",
|
||||
"editServerTitle": "Изменить сервер",
|
||||
"addServerTitle": "Добавить сервер",
|
||||
"titleManageProfilesShort": "Профили",
|
||||
"descriptionStreamerMode": "При включении этого параметра, внешний вид некоторых элементов становится более приватным, скрывая длинные Onion адреса и адреса контактов, оставляя только заданные имена",
|
||||
"descriptionFileSharing": "Данная функция позволяет обмениваться файлами напрямую с контактами и группами в Cwtch. Отправляемый файл будет напрямую скачиваться с вашего устройства через Cwtch.",
|
||||
"settingFileSharing": "Передача файлов",
|
||||
"tooltipSendFile": "Отправить файл",
|
||||
"messageFileOffered": "Контакт предлагает загрузить вам файл",
|
||||
"messageFileSent": "Вы отправили файл",
|
||||
"messageEnableFileSharing": "Включите экспериментальную функцию Обмен файлами чтобы просмотреть это сообщение.",
|
||||
"labelFilesize": "Размер",
|
||||
"labelFilename": "Имя-файла",
|
||||
"downloadFileButton": "Загрузить",
|
||||
"openFolderButton": "Открыть папку",
|
||||
"retrievingManifestMessage": "Получение информации о файле...",
|
||||
"streamerModeLabel": "Режим презентации",
|
||||
"archiveConversation": "Отправить чат в архив",
|
||||
"profileOnionLabel": "Send this address to contacts you want to connect with",
|
||||
"addPeerTab": "Добавить контакт",
|
||||
"addPeer": "Добавить контакт",
|
||||
"peerNotOnline": "Контакт не в сети. Вы не можете связаться с ним пока он не появиться в сети.",
|
||||
"peerBlockedMessage": "Контакт заблокирован",
|
||||
"peerOfflineMessage": "Контакт не в сети, сообщения не могут быть отправлены",
|
||||
"blockBtn": "Заблокировать контакт",
|
||||
"savePeerHistory": "Хранить исторую",
|
||||
"savePeerHistoryDescription": "Определяет политуку хранения или удаления переписки с данным контактом.",
|
||||
"dontSavePeerHistory": "Удалить историю",
|
||||
"unblockBtn": "Разблокировать контакт",
|
||||
"blockUnknownLabel": "Блокировать неизвестные контакты",
|
||||
"blockUnknownConnectionsEnabledDescription": "Соединения от неизвестных контактов блокируются. Данный параметр можно изменить в настройках",
|
||||
"networkStatusConnecting": "Подключение к сети и контактам...",
|
||||
"showMessageButton": "Показать сообщения",
|
||||
"blockedMessageMessage": "Это сообщение из заблокированного вами профиля.",
|
||||
"placeholderEnterMessage": "Написать сообщение...",
|
||||
"plainProfileDescription": "Мы рекомендуем защитить свой ПРОФИЛЬ Cwtch паролем. Если Вы этого не сделаете, то любой у кого окажется доступ к устройству, сможет получить доступ к информации об этом профиле, включая контакты, сообщения и конфиденциальные криптографические ключи.",
|
||||
"encryptedProfileDescription": "Шифрование ПРОФИЛЯ паролем защитит его от других людей у которых может оказаться доступ к этому устройству. Зашифрованный ПРОФИЛЬ нельзя расшифровать, пока не будет введен правильный пароль разблокировки.",
|
||||
"addContactConfirm": "Добавить контакт %1",
|
||||
"addContact": "Добавить контакт",
|
||||
"contactGoto": "Перейти к сообщению от %1",
|
||||
"settingUIColumnOptionSame": "Как в настройках портретного режима",
|
||||
"settingUIColumnDouble14Ratio": "Двойной (1:4)",
|
||||
"settingUIColumnDouble12Ratio": "Двойной (1:2)",
|
||||
"settingUIColumnSingle": "Одиночный",
|
||||
"settingUIColumnLandscape": "UI столбцы в Ландшафтном Режиме",
|
||||
"settingUIColumnPortrait": "UI столбцы в Портретном режиме",
|
||||
"localePl": "Польский",
|
||||
"tooltipRemoveThisQuotedMessage": "Удалить цитируемое сообщение.",
|
||||
"tooltipReplyToThisMessage": "Ответить на это сообщение",
|
||||
"tooltipRejectContactRequest": "Отклонить запрос в контакты.",
|
||||
"tooltipAcceptContactRequest": "Принять запрос в контакты.",
|
||||
"notificationNewMessageFromGroup": "Новое сообщение в группе!",
|
||||
"notificationNewMessageFromPeer": "Новое сообщение от контакта!",
|
||||
"tooltipHidePassword": "Скрыть пароль",
|
||||
"tooltipShowPassword": "Показать пароль",
|
||||
"serverNotSynced": "Синхронизация новых сообщений (это может занять некоторое время)...",
|
||||
"groupInviteSettingsWarning": "Вас пригласили присоединиться к группе! Пожалуйста, включите экспериментальную функцию групповые чаты в Настройках, чтобы просмотреть это приглашение.",
|
||||
"shutdownCwtchAction": "Выключить Cwtch",
|
||||
"shutdownCwtchDialog": "Вы уверены, что хотите выключить Cwtch? Это приведет к закрытию всех подключений и выходу из приложения.",
|
||||
"shutdownCwtchDialogTitle": "Выключить Cwtch?",
|
||||
"shutdownCwtchTooltip": "Выключить Cwtch",
|
||||
"malformedMessage": "Некорректное сообщение",
|
||||
"profileDeleteSuccess": "Профиль успешно удален",
|
||||
"debugLog": "Влючить отладку через консоль",
|
||||
"torNetworkStatus": "Статус сети Tor",
|
||||
"addContactFirst": "Добавьте или выберите контакт, чтобы начать чат.",
|
||||
"createProfileToBegin": "Пожалуйста, создайте или разблокируйте профиль, чтобы начать",
|
||||
"nickChangeSuccess": "Имя профиля успешно изменено",
|
||||
"addServerFirst": "Перед созданием группы, необходимо создать сервер",
|
||||
"deleteProfileSuccess": "Профиль успешно удален",
|
||||
"sendInvite": "Отправить контакт или приглашение в группу",
|
||||
"sendMessage": "Отправить сообщение",
|
||||
"cancel": "Отмена",
|
||||
"resetTor": "Сбросс",
|
||||
"torStatus": "Статус Tor",
|
||||
"torVersion": "Версия Tor",
|
||||
"sendAnInvitation": "Вы отправили приглашение для: ",
|
||||
"contactSuggestion": "Вам предложили этот контакт: ",
|
||||
"rejected": "Отклонить!",
|
||||
"accepted": "Принять!",
|
||||
"chatHistoryDefault": "Этот чат будет удален после закрытия Cwtch! Историю сообщений можно включить для каждого чата отдельно через меню настроек в правом верхнем углу..",
|
||||
"newPassword": "Новый пароль",
|
||||
"yesLeave": "Да, оставить этот чат",
|
||||
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
|
||||
"leaveGroup": "Да, оставить этот чат",
|
||||
"inviteToGroup": "Вас пригласили присоединиться к группе:",
|
||||
"pasteAddressToAddContact": "Вставьте адрес cwtch, приглашение или пакет ключей здесь, чтобы добавить их в контакты",
|
||||
"tooltipAddContact": "Добавление нового контакта или разговора",
|
||||
"titleManageContacts": "Разговоры",
|
||||
"titleManageServers": "Управление серверами",
|
||||
"dateNever": "Никогда",
|
||||
"dateLastYear": "Прошлый год",
|
||||
"dateYesterday": "Вчера",
|
||||
"dateLastMonth": "Прошлый месяц",
|
||||
"dateRightNow": "Прямо сейчас",
|
||||
"successfullAddedContact": "Успешно добавлен",
|
||||
"descriptionBlockUnknownConnections": "Если включить этот параметр, все соединения от людей не состоящих в ваших контактах будут отклонены.",
|
||||
"descriptionExperimentsGroups": "Данная экспериментальная функция позволяет Cwtch подключаться к недоверенной серверной инфраструктуре, чтобы облегчить Вам общение с более чем одним контактом.",
|
||||
"descriptionExperiments": "Экспериментальные функции Cwtch это необязательные дополнительные функции, которые добавляют некоторые возможности, но не имеют такой же устойчивости к метаданным как если бы вы общались через традиционный част 1 на 1..",
|
||||
"titleManageProfiles": "Управление профилями Cwtch",
|
||||
"tooltipUnlockProfiles": "Разблокировать зашифрованные профили, введя их пароль.",
|
||||
"tooltipOpenSettings": "Откройте панель настроек",
|
||||
"invalidImportString": "Недействительная строка импорта",
|
||||
"contactAlreadyExists": "Контакт уже существует",
|
||||
"conversationSettings": "Настройки чата",
|
||||
"enterCurrentPasswordForDelete": "Пожалуйста, введите текущий пароль, чтобы удалить этот профиль.",
|
||||
"enableGroups": "Включить Групповые чаты",
|
||||
"experimentsEnabled": "Включить Экспериментальные функции",
|
||||
"localeIt": "Итальянский",
|
||||
"localeEs": "Испанский",
|
||||
"addListItem": "Добавить новый элемент",
|
||||
"addNewItem": "Добавить новый элемент в список",
|
||||
"todoPlaceholder": "Выполняю...",
|
||||
"newConnectionPaneTitle": "Новое соединение",
|
||||
"networkStatusOnline": "Online",
|
||||
"networkStatusAttemptingTor": "Попытка подключиться к сети Tor",
|
||||
"networkStatusDisconnected": "Нет сети. Проверьте подключение к интернету",
|
||||
"viewGroupMembershipTooltip": "Просмотр членства в группе",
|
||||
"loadingTor": "Загрузка Tor...",
|
||||
"smallTextLabel": "Маленький",
|
||||
"defaultScalingText": "Размер текста по умолчанию (коэффициент масштабирования:",
|
||||
"builddate": "Построен на: %2",
|
||||
"version": "Версия %1",
|
||||
"versionTor": "Версия %1 c tor %2",
|
||||
"themeDark": "Темная",
|
||||
"themeLight": "Светлая",
|
||||
"settingTheme": "Тема",
|
||||
"largeTextLabel": "Большой",
|
||||
"settingInterfaceZoom": "Уровень масштабирования",
|
||||
"localeDe": "Немецкий",
|
||||
"localePt": "Португальский",
|
||||
"localeFr": "Французский",
|
||||
"localeEn": "Английский",
|
||||
"settingLanguage": "Язык",
|
||||
"zoomLabel": "Масштаб интерфейса (в основном влияет на размеры текста и кнопок)",
|
||||
"versionBuilddate": "Версия: %1 Сборка от: %2",
|
||||
"cwtchSettingsTitle": "Настройки Cwtch",
|
||||
"unlock": "Разблокировать",
|
||||
"yourServers": "Ваши Серверы",
|
||||
"yourProfiles": "Ваши Профили",
|
||||
"error0ProfilesLoadedForPassword": "0 профилей, загруженных с этим паролем",
|
||||
"password": "Пароль",
|
||||
"enterProfilePassword": "Введите пароль для просмотра ваших профилей",
|
||||
"addNewProfileBtn": "Добавить новый профиль",
|
||||
"deleteConfirmText": "УДАЛИТЬ",
|
||||
"deleteProfileConfirmBtn": "Действительно удалить профиль?",
|
||||
"deleteConfirmLabel": "Введите DELETE чтобы продолжить",
|
||||
"deleteProfileBtn": "Удалить профиль",
|
||||
"passwordChangeError": "Ошибка при смене пароля: Введенный пароль отклонен",
|
||||
"passwordErrorMatch": "Пароли не совпадают",
|
||||
"saveProfileBtn": "Сохранить профиль",
|
||||
"createProfileBtn": "Создать профиль",
|
||||
"passwordErrorEmpty": "Пароль не может быть пустым",
|
||||
"password2Label": "Повторный ввод пароля",
|
||||
"password1Label": "Пароль",
|
||||
"currentPasswordLabel": "Текущий пароль",
|
||||
"yourDisplayName": "Отображаемое имя",
|
||||
"noPasswordWarning": "Отсутствие пароля в этой учетной записи означает, что все данные, хранящиеся локально, не будут зашифрованы",
|
||||
"radioNoPassword": "Незашифрованный (без пароля)",
|
||||
"radioUsePassword": "Пароль",
|
||||
"copiedToClipboardNotification": "Copied to Clipboard",
|
||||
"copyBtn": "Copy",
|
||||
"editProfile": "Изменить профиль",
|
||||
"newProfile": "Новый профиль",
|
||||
"defaultProfileName": "Alice",
|
||||
"profileName": "Отображаемое имя",
|
||||
"editProfileTitle": "Изменить профиль",
|
||||
"addProfileTitle": "Добавить новый профиль",
|
||||
"deleteBtn": "Delete",
|
||||
"saveBtn": "Save",
|
||||
"displayNameLabel": "Отображаемое имя",
|
||||
"addressLabel": "Адрес",
|
||||
"puzzleGameBtn": "Puzzle Game",
|
||||
"bulletinsBtn": "Bulletins",
|
||||
"listsBtn": "Списки",
|
||||
"chatBtn": "Чат",
|
||||
"rejectGroupBtn": "Отклонить",
|
||||
"acceptGroupBtn": "Принять",
|
||||
"acceptGroupInviteLabel": "Хотите принять приглашение в",
|
||||
"newGroupBtn": "Создать новую группу",
|
||||
"copiedClipboardNotification": "Скопировано в буфер обмена",
|
||||
"pendingLabel": "Ожидаемый",
|
||||
"acknowledgedLabel": "Отправлено",
|
||||
"couldNotSendMsgError": "Не удалось отправить это сообщение",
|
||||
"dmTooltip": "Нажмите, чтобы перейти в DM",
|
||||
"membershipDescription": "Ниже приведен список пользователей, отправивших сообщения группе. Этот список может не отражать всех пользователей, имеющих доступ к группе.",
|
||||
"addListItemBtn": "Добавить элемент",
|
||||
"searchList": "Список поиска",
|
||||
"update": "Обновить",
|
||||
"inviteBtn": "Пригласить",
|
||||
"inviteToGroupLabel": "Пригласить в группу",
|
||||
"groupNameLabel": "Group name",
|
||||
"viewServerInfo": "Информация о сервере",
|
||||
"serverSynced": "Синхронизировано",
|
||||
"serverConnectivityDisconnected": "Сервер отключен",
|
||||
"serverConnectivityConnected": "Сервер подключен",
|
||||
"serverInfo": "Информация о сервере",
|
||||
"invitationLabel": "Приглашение",
|
||||
"serverLabel": "Server",
|
||||
"search": "Поиск...",
|
||||
"cycleColoursDesktop": "Нажмите, чтобы переключать цвета.\nПравый клик чтобы сбросить.",
|
||||
"cycleColoursAndroid": "Нажмите, чтобы переключать цвета.\nНажмите и удерживайте, чтобы сбросить.",
|
||||
"cycleMorphsDesktop": "Нажмите, чтобы просмотреть формы.\nПравый клик чтобы сбросить.",
|
||||
"cycleMorphsAndroid": "Нажмите, чтобы просмотреть формы.\nНажмите и удерживайте, чтобы сбросить.",
|
||||
"cycleCatsDesktop": "Нажмите, чтобы просмотреть категории.\nПравый клик чтобы сбросить.",
|
||||
"cycleCatsAndroid": "Нажмите, чтобы просмотреть категории.\nНажмите и удерживайте, чтобы сбросить.",
|
||||
"blocked": "Заблокировано",
|
||||
"titlePlaceholder": "заговолок...",
|
||||
"postNewBulletinLabel": "Опубликовать новый бюллетень",
|
||||
"newBulletinLabel": "Новый бюллетень",
|
||||
"joinGroup": "Вступить в группу",
|
||||
"createGroup": "Создать группу",
|
||||
"groupAddr": "Адрес",
|
||||
"invitation": "Приглашение",
|
||||
"server": "Сервер",
|
||||
"groupName": "Имя группы",
|
||||
"peerName": "Имя",
|
||||
"peerAddress": "Адрес",
|
||||
"joinGroupTab": "Присоединиться к группе",
|
||||
"createGroupTab": "Создать группу",
|
||||
"createGroupBtn": "Создать",
|
||||
"defaultGroupName": "Замечательная группа",
|
||||
"createGroupTitle": "Создать группу"
|
||||
}
|
|
@ -17,6 +17,7 @@ import 'cwtch/cwtch.dart';
|
|||
import 'cwtch/cwtchNotifier.dart';
|
||||
import 'licenses.dart';
|
||||
import 'model.dart';
|
||||
import 'models/servers.dart';
|
||||
import 'views/profilemgrview.dart';
|
||||
import 'views/splashView.dart';
|
||||
import 'dart:io' show Platform, exit;
|
||||
|
@ -27,6 +28,7 @@ var globalSettings = Settings(Locale("en", ''), OpaqueDark());
|
|||
var globalErrorHandler = ErrorHandler();
|
||||
var globalTorStatus = TorStatus();
|
||||
var globalAppState = AppState();
|
||||
var globalServersList = ServerListState();
|
||||
|
||||
void main() {
|
||||
print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}");
|
||||
|
@ -62,13 +64,13 @@ class FlwtchState extends State<Flwtch> {
|
|||
shutdownMethodChannel.setMethodCallHandler(modalShutdown);
|
||||
print("initState: creating cwtchnotifier, ffi");
|
||||
if (Platform.isAndroid) {
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
|
||||
cwtch = CwtchGomobile(cwtchNotifier);
|
||||
} else if (Platform.isLinux) {
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState);
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(), globalAppState, globalServersList);
|
||||
cwtch = CwtchFfi(cwtchNotifier);
|
||||
} else {
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState);
|
||||
var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList);
|
||||
cwtch = CwtchFfi(cwtchNotifier);
|
||||
}
|
||||
print("initState: invoking cwtch.Start()");
|
||||
|
@ -82,6 +84,7 @@ class FlwtchState extends State<Flwtch> {
|
|||
ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
|
||||
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
|
||||
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
|
||||
ChangeNotifierProvider<ServerListState> getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -94,6 +97,7 @@ class FlwtchState extends State<Flwtch> {
|
|||
getErrorHandlerProvider(),
|
||||
getTorStatusProvider(),
|
||||
getAppStateProvider(),
|
||||
getServerListStateProvider(),
|
||||
],
|
||||
builder: (context, widget) {
|
||||
return Consumer2<Settings, AppState>(
|
||||
|
@ -116,13 +120,13 @@ class FlwtchState extends State<Flwtch> {
|
|||
// the MyBroadcastReceiver method channel
|
||||
Future<void> modalShutdown(MethodCall mc) async {
|
||||
// set up the buttons
|
||||
Widget cancelButton = TextButton(
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(navKey.currentContext!)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = TextButton(
|
||||
Widget continueButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction),
|
||||
onPressed: () {
|
||||
// Directly call the shutdown command, Android will do this for us...
|
||||
|
@ -156,7 +160,7 @@ class FlwtchState extends State<Flwtch> {
|
|||
Future.delayed(Duration(seconds: 2)).then((value) {
|
||||
if (Platform.isAndroid) {
|
||||
SystemNavigator.pop();
|
||||
} else if (Platform.isLinux || Platform.isWindows) {
|
||||
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
print("Exiting...");
|
||||
exit(0);
|
||||
}
|
||||
|
@ -169,7 +173,7 @@ class FlwtchState extends State<Flwtch> {
|
|||
var args = jsonDecode(call.arguments);
|
||||
var profile = profs.getProfile(args["ProfileOnion"])!;
|
||||
var convo = profile.contactList.getContact(args["Handle"])!;
|
||||
var initialIndex = convo.unreadMessages;
|
||||
Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
|
||||
convo.unreadMessages = 0;
|
||||
|
||||
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
|
||||
|
@ -187,7 +191,7 @@ class FlwtchState extends State<Flwtch> {
|
|||
ChangeNotifierProvider.value(value: profile),
|
||||
ChangeNotifierProvider.value(value: convo),
|
||||
],
|
||||
builder: (context, child) => MessageView(initialIndex),
|
||||
builder: (context, child) => MessageView(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
218
lib/model.dart
|
@ -2,7 +2,7 @@ import 'dart:convert';
|
|||
|
||||
import 'package:cwtch/widgets/messagerow.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/models/profileservers.dart';
|
||||
|
||||
////////////////////
|
||||
/// UI State ///
|
||||
|
@ -30,6 +30,8 @@ class AppState extends ChangeNotifier {
|
|||
String appError = "";
|
||||
String? _selectedProfile;
|
||||
String? _selectedConversation;
|
||||
int _initialScrollIndex = 0;
|
||||
int _hoveredIndex = -1;
|
||||
int? _selectedIndex;
|
||||
bool _unreadMessagesBelow = false;
|
||||
|
||||
|
@ -61,12 +63,26 @@ class AppState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
// Never use this for message lookup - can be a non-indexed value
|
||||
// e.g. -1
|
||||
int get hoveredIndex => _hoveredIndex;
|
||||
set hoveredIndex(int newVal) {
|
||||
this._hoveredIndex = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get unreadMessagesBelow => _unreadMessagesBelow;
|
||||
set unreadMessagesBelow(bool newVal) {
|
||||
this._unreadMessagesBelow = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get initialScrollIndex => _initialScrollIndex;
|
||||
set initialScrollIndex(int newVal) {
|
||||
this._initialScrollIndex = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
|
||||
}
|
||||
|
||||
|
@ -135,6 +151,9 @@ class ContactListState extends ChangeNotifier {
|
|||
// blocked contacts last
|
||||
if (a.isBlocked == true && b.isBlocked != true) return 1;
|
||||
if (a.isBlocked != true && b.isBlocked == true) return -1;
|
||||
// archive is next...
|
||||
if (!a.isArchived && b.isArchived) return -1;
|
||||
if (a.isArchived && !b.isArchived) return 1;
|
||||
// special sorting for contacts with no messages in either history
|
||||
if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) {
|
||||
// online contacts first
|
||||
|
@ -188,12 +207,13 @@ class ContactListState extends ChangeNotifier {
|
|||
|
||||
class ProfileInfoState extends ChangeNotifier {
|
||||
ContactListState _contacts = ContactListState();
|
||||
ServerListState _servers = ServerListState();
|
||||
ProfileServerListState _servers = ProfileServerListState();
|
||||
final String onion;
|
||||
String _nickname = "";
|
||||
String _imagePath = "";
|
||||
int _unreadMessages = 0;
|
||||
bool _online = false;
|
||||
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||
|
||||
// assume profiles are encrypted...this will be set to false
|
||||
// in the constructor if the profile is encrypted with the defacto password.
|
||||
|
@ -228,6 +248,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
numUnread: contact["numUnread"],
|
||||
isGroup: contact["isGroup"],
|
||||
server: contact["groupServer"],
|
||||
archived: contact["isArchived"] == true,
|
||||
lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])));
|
||||
}));
|
||||
|
||||
|
@ -246,7 +267,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
List<dynamic> servers = jsonDecode(serversJson);
|
||||
this._servers.replace(servers.map((server) {
|
||||
// TODO Keys...
|
||||
return ServerInfoState(onion: server["onion"], status: server["status"]);
|
||||
return RemoteServerInfoState(onion: server["onion"], status: server["status"]);
|
||||
}));
|
||||
notifyListeners();
|
||||
}
|
||||
|
@ -295,7 +316,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
}
|
||||
|
||||
ContactListState get contactList => this._contacts;
|
||||
ServerListState get serverList => this._servers;
|
||||
ProfileServerListState get serverList => this._servers;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
@ -336,6 +357,128 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
void downloadInit(String fileKey, int numChunks) {
|
||||
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
|
||||
}
|
||||
|
||||
void downloadUpdate(String fileKey, int progress, int numChunks) {
|
||||
if (!downloadActive(fileKey)) {
|
||||
if (progress < 0) {
|
||||
this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
|
||||
this._downloads[fileKey]!.interrupted = true;
|
||||
notifyListeners();
|
||||
} else {
|
||||
print("error: received progress for unknown download " + fileKey);
|
||||
}
|
||||
} else {
|
||||
if (this._downloads[fileKey]!.interrupted) {
|
||||
this._downloads[fileKey]!.interrupted = false;
|
||||
}
|
||||
this._downloads[fileKey]!.chunksDownloaded = progress;
|
||||
this._downloads[fileKey]!.chunksTotal = numChunks;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void downloadMarkManifest(String fileKey) {
|
||||
if (!downloadActive(fileKey)) {
|
||||
print("error: received download completion notice for unknown download " + fileKey);
|
||||
} else {
|
||||
this._downloads[fileKey]!.gotManifest = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void downloadMarkFinished(String fileKey, String finalPath) {
|
||||
if (!downloadActive(fileKey)) {
|
||||
// happens as a result of a CheckDownloadStatus call,
|
||||
// invoked from a historical (timeline) download message
|
||||
// so setting numChunks correctly shouldn't matter
|
||||
this.downloadInit(fileKey, 1);
|
||||
}
|
||||
this._downloads[fileKey]!.timeEnd = DateTime.now();
|
||||
this._downloads[fileKey]!.downloadedTo = finalPath;
|
||||
this._downloads[fileKey]!.complete = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool downloadActive(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted;
|
||||
}
|
||||
|
||||
bool downloadGotManifest(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest;
|
||||
}
|
||||
|
||||
bool downloadComplete(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete;
|
||||
}
|
||||
|
||||
bool downloadInterrupted(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted;
|
||||
}
|
||||
|
||||
void downloadMarkResumed(String fileKey) {
|
||||
if (this._downloads.containsKey(fileKey)) {
|
||||
this._downloads[fileKey]!.interrupted = false;
|
||||
}
|
||||
}
|
||||
|
||||
double downloadProgress(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0;
|
||||
}
|
||||
|
||||
// used for loading interrupted download info; use downloadMarkFinished for successful downloads
|
||||
void downloadSetPath(String fileKey, String path) {
|
||||
if (this._downloads.containsKey(fileKey)) {
|
||||
this._downloads[fileKey]!.downloadedTo = path;
|
||||
}
|
||||
}
|
||||
|
||||
String? downloadFinalPath(String fileKey) {
|
||||
return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null;
|
||||
}
|
||||
|
||||
String downloadSpeed(String fileKey) {
|
||||
if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) {
|
||||
return "0 B/s";
|
||||
}
|
||||
var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096;
|
||||
var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds;
|
||||
if (seconds == 0) {
|
||||
return "0 B/s";
|
||||
}
|
||||
return prettyBytes((bytes / seconds).round()) + "/s";
|
||||
}
|
||||
}
|
||||
|
||||
class FileDownloadProgress {
|
||||
int chunksDownloaded = 0;
|
||||
int chunksTotal = 1;
|
||||
bool complete = false;
|
||||
bool gotManifest = false;
|
||||
bool interrupted = false;
|
||||
String? downloadedTo;
|
||||
DateTime? timeStart;
|
||||
DateTime? timeEnd;
|
||||
|
||||
FileDownloadProgress(this.chunksTotal, this.timeStart);
|
||||
double progress() {
|
||||
return 1.0 * chunksDownloaded / chunksTotal;
|
||||
}
|
||||
}
|
||||
|
||||
String prettyBytes(int bytes) {
|
||||
if (bytes > 1000000000) {
|
||||
return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB";
|
||||
} else if (bytes > 1000000) {
|
||||
return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB";
|
||||
} else if (bytes > 1000) {
|
||||
return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB";
|
||||
} else {
|
||||
return bytes.toString() + " B";
|
||||
}
|
||||
}
|
||||
|
||||
enum ContactAuthorization { unknown, approved, blocked }
|
||||
|
@ -364,25 +507,26 @@ class ContactInfoState extends ChangeNotifier {
|
|||
late int _totalMessages = 0;
|
||||
late DateTime _lastMessageTime;
|
||||
late Map<String, GlobalKey<MessageRowState>> keys;
|
||||
int _newMarker = 0;
|
||||
DateTime _newMarkerClearAt = DateTime.now();
|
||||
|
||||
// todo: a nicer way to model contacts, groups and other "entities"
|
||||
late bool _isGroup;
|
||||
String? _server;
|
||||
late bool _archived;
|
||||
|
||||
ContactInfoState(
|
||||
this.profileOnion,
|
||||
this.onion, {
|
||||
nickname = "",
|
||||
isGroup = false,
|
||||
authorization = ContactAuthorization.unknown,
|
||||
status = "",
|
||||
imagePath = "",
|
||||
savePeerHistory = "DeleteHistoryConfirmed",
|
||||
numMessages = 0,
|
||||
numUnread = 0,
|
||||
lastMessageTime,
|
||||
server,
|
||||
}) {
|
||||
ContactInfoState(this.profileOnion, this.onion,
|
||||
{nickname = "",
|
||||
isGroup = false,
|
||||
authorization = ContactAuthorization.unknown,
|
||||
status = "",
|
||||
imagePath = "",
|
||||
savePeerHistory = "DeleteHistoryConfirmed",
|
||||
numMessages = 0,
|
||||
numUnread = 0,
|
||||
lastMessageTime,
|
||||
server,
|
||||
archived = false}) {
|
||||
this._nickname = nickname;
|
||||
this._isGroup = isGroup;
|
||||
this._authorization = authorization;
|
||||
|
@ -393,12 +537,24 @@ class ContactInfoState extends ChangeNotifier {
|
|||
this._savePeerHistory = savePeerHistory;
|
||||
this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
|
||||
this._server = server;
|
||||
this._archived = archived;
|
||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||
}
|
||||
|
||||
String get nickname => this._nickname;
|
||||
|
||||
String get savePeerHistory => this._savePeerHistory;
|
||||
|
||||
// Indicated whether the conversation is archived, in which case it will
|
||||
// be moved to the very bottom of the active conversations list until
|
||||
// new messages appear
|
||||
set isArchived(bool archived) {
|
||||
this._archived = archived;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get isArchived => this._archived;
|
||||
|
||||
set savePeerHistory(String newVal) {
|
||||
this._savePeerHistory = newVal;
|
||||
notifyListeners();
|
||||
|
@ -433,10 +589,36 @@ class ContactInfoState extends ChangeNotifier {
|
|||
|
||||
int get unreadMessages => this._unreadMessages;
|
||||
set unreadMessages(int newVal) {
|
||||
// don't reset newMarker position when unreadMessages is being cleared
|
||||
if (newVal > 0) {
|
||||
this._newMarker = newVal;
|
||||
} else {
|
||||
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes:2));
|
||||
}
|
||||
this._unreadMessages = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get newMarker {
|
||||
if (DateTime.now().isAfter(this._newMarkerClearAt)) {
|
||||
// perform heresy
|
||||
this._newMarker = 0;
|
||||
// no need to notifyListeners() because presumably this getter is
|
||||
// being called from a renderer anyway
|
||||
}
|
||||
return this._newMarker;
|
||||
}
|
||||
// what's a getter that sometimes sets without a setter
|
||||
// that sometimes doesn't set
|
||||
set newMarker(int newVal) {
|
||||
// only unreadMessages++ can set newMarker = 1;
|
||||
// avoids drawing a marker when the convo is already open
|
||||
if (newVal > 1) {
|
||||
this._newMarker = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
int get totalMessages => this._totalMessages;
|
||||
set totalMessages(int newVal) {
|
||||
this._totalMessages = newVal;
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
import '../main.dart';
|
||||
import '../model.dart';
|
||||
import 'messages/filemessage.dart';
|
||||
import 'messages/invitemessage.dart';
|
||||
import 'messages/malformedmessage.dart';
|
||||
import 'messages/quotedmessage.dart';
|
||||
|
@ -14,6 +15,7 @@ const TextMessageOverlay = 1;
|
|||
const QuotedMessageOverlay = 10;
|
||||
const SuggestContactOverlay = 100;
|
||||
const InviteGroupOverlay = 101;
|
||||
const FileShareOverlay = 200;
|
||||
|
||||
// Defines the length of the tor v3 onion address. Code using this constant will
|
||||
// need to updated when we allow multiple different identifiers. At which time
|
||||
|
@ -33,38 +35,39 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
|
|||
try {
|
||||
var rawMessageEnvelopeFuture = Provider.of<FlwtchState>(context, listen: false).cwtch.GetMessage(profileOnion, contactHandle, index);
|
||||
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) {
|
||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
||||
// There are 2 conditions in which this error condition can be met:
|
||||
// 1. The application == nil, in which case this instance of the UI is already
|
||||
// broken beyond repair, and will either be replaced by a new version, or requires a complete
|
||||
// restart.
|
||||
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
|
||||
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
|
||||
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
|
||||
// will find itself delayed.
|
||||
// The second case is recoverable by tail-recursing this future.
|
||||
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
||||
return Future.delayed(Duration(seconds: 2), () {
|
||||
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
|
||||
return messageHandler(context, profileOnion, contactHandle, index).then((value) => value);
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the initial metadata
|
||||
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
|
||||
var senderHandle = messageWrapper['PeerID'];
|
||||
var senderImage = messageWrapper['ContactImage'];
|
||||
var flags = int.parse(messageWrapper['Flags'].toString(), radix: 2);
|
||||
var ackd = messageWrapper['Acknowledged'];
|
||||
var error = messageWrapper['Error'] != null;
|
||||
String? signature;
|
||||
// If this is a group, store the signature
|
||||
if (contactHandle.length == GroupConversationHandleLength) {
|
||||
signature = messageWrapper['Signature'];
|
||||
}
|
||||
var metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
|
||||
|
||||
var metadata = MessageMetadata(profileOnion, contactHandle, index, DateTime.now(), "", "", null, 0, false, true);
|
||||
try {
|
||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
||||
// There are 2 conditions in which this error condition can be met:
|
||||
// 1. The application == nil, in which case this instance of the UI is already
|
||||
// broken beyond repair, and will either be replaced by a new version, or requires a complete
|
||||
// restart.
|
||||
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
|
||||
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
|
||||
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
|
||||
// will find itself delayed.
|
||||
// The second case is recoverable by tail-recursing this future.
|
||||
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
|
||||
return Future.delayed(Duration(seconds: 2), () {
|
||||
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
|
||||
return messageHandler(context, profileOnion, contactHandle, index).then((value) => value);
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the initial metadata
|
||||
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
|
||||
var senderHandle = messageWrapper['PeerID'];
|
||||
var senderImage = messageWrapper['ContactImage'];
|
||||
var flags = int.parse(messageWrapper['Flags'].toString());
|
||||
var ackd = messageWrapper['Acknowledged'];
|
||||
var error = messageWrapper['Error'] != null;
|
||||
String? signature;
|
||||
// If this is a group, store the signature
|
||||
if (contactHandle.length == GroupConversationHandleLength) {
|
||||
signature = messageWrapper['Signature'];
|
||||
}
|
||||
metadata = MessageMetadata(profileOnion, contactHandle, index, timestamp, senderHandle, senderImage, signature, flags, ackd, error);
|
||||
|
||||
dynamic message = jsonDecode(messageWrapper['Message']);
|
||||
var content = message['d'] as dynamic;
|
||||
var overlay = int.parse(message['o'].toString());
|
||||
|
@ -77,11 +80,14 @@ Future<Message> messageHandler(BuildContext context, String profileOnion, String
|
|||
return InviteMessage(overlay, metadata, content);
|
||||
case QuotedMessageOverlay:
|
||||
return QuotedMessage(metadata, content);
|
||||
case FileShareOverlay:
|
||||
return FileMessage(metadata, content);
|
||||
default:
|
||||
// Metadata is valid, content is not..
|
||||
return MalformedMessage(metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
print("an error! " + e.toString());
|
||||
return MalformedMessage(metadata);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:cwtch/models/message.dart';
|
||||
import 'package:cwtch/widgets/filebubble.dart';
|
||||
import 'package:cwtch/widgets/malformedbubble.dart';
|
||||
import 'package:cwtch/widgets/messagerow.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../model.dart';
|
||||
|
||||
class FileMessage extends Message {
|
||||
final MessageMetadata metadata;
|
||||
final String content;
|
||||
final RegExp nonHex = RegExp(r'[^a-f0-9]');
|
||||
|
||||
FileMessage(this.metadata, this.content);
|
||||
|
||||
@override
|
||||
Widget getWidget(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: this.metadata,
|
||||
builder: (bcontext, child) {
|
||||
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
|
||||
dynamic shareObj = jsonDecode(this.content);
|
||||
if (shareObj == null) {
|
||||
return MessageRow(MalformedBubble());
|
||||
}
|
||||
String nameSuggestion = shareObj['f'] as String;
|
||||
String rootHash = shareObj['h'] as String;
|
||||
String nonce = shareObj['n'] as String;
|
||||
int fileSize = shareObj['s'] as int;
|
||||
|
||||
if (!validHash(rootHash, nonce)) {
|
||||
return MessageRow(MalformedBubble());
|
||||
}
|
||||
|
||||
return MessageRow(FileBubble(nameSuggestion, rootHash, nonce, fileSize), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget getPreviewWidget(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: this.metadata,
|
||||
builder: (bcontext, child) {
|
||||
dynamic shareObj = jsonDecode(this.content);
|
||||
if (shareObj == null) {
|
||||
return MessageRow(MalformedBubble());
|
||||
}
|
||||
String nameSuggestion = shareObj['n'] as String;
|
||||
String rootHash = shareObj['h'] as String;
|
||||
String nonce = shareObj['n'] as String;
|
||||
int fileSize = shareObj['s'] as int;
|
||||
if (!validHash(rootHash, nonce)) {
|
||||
return MessageRow(MalformedBubble());
|
||||
}
|
||||
return FileBubble(
|
||||
nameSuggestion,
|
||||
rootHash,
|
||||
nonce,
|
||||
fileSize,
|
||||
interactive: false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
MessageMetadata getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
bool validHash(String hash, String nonce) {
|
||||
return hash.length == 128 && nonce.length == 48 && !hash.contains(nonHex) && !nonce.contains(nonHex);
|
||||
}
|
||||
}
|
|
@ -21,8 +21,7 @@ class InviteMessage extends Message {
|
|||
return ChangeNotifierProvider.value(
|
||||
value: this.metadata,
|
||||
builder: (bcontext, child) {
|
||||
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
|
||||
|
||||
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
|
||||
String inviteTarget;
|
||||
String inviteNick;
|
||||
String invite = this.content;
|
||||
|
|
|
@ -94,7 +94,7 @@ class QuotedMessage extends Message {
|
|||
return ChangeNotifierProvider.value(
|
||||
value: this.metadata,
|
||||
builder: (bcontext, child) {
|
||||
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
|
||||
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
|
||||
return MessageRow(
|
||||
QuotedMessageBubble(message["body"], quotedMessage.then((LocallyIndexedMessage? localIndex) {
|
||||
if (localIndex != null) {
|
||||
|
|
|
@ -32,7 +32,7 @@ class TextMessage extends Message {
|
|||
return ChangeNotifierProvider.value(
|
||||
value: this.metadata,
|
||||
builder: (bcontext, child) {
|
||||
String idx = Provider.of<ContactInfoState>(context).isGroup == true && this.metadata.signature != null ? this.metadata.signature! : this.metadata.messageIndex.toString();
|
||||
String idx = this.metadata.contactHandle + this.metadata.messageIndex.toString();
|
||||
return MessageRow(MessageBubble(this.content), key: Provider.of<ContactInfoState>(bcontext).getMessageKey(idx));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfileServerListState extends ChangeNotifier {
|
||||
List<RemoteServerInfoState> _servers = [];
|
||||
|
||||
void replace(Iterable<RemoteServerInfoState> newServers) {
|
||||
_servers.clear();
|
||||
_servers.addAll(newServers);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
RemoteServerInfoState? getServer(String onion) {
|
||||
int idx = _servers.indexWhere((element) => element.onion == onion);
|
||||
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] = RemoteServerInfoState(onion: onion, status: status);
|
||||
} else {
|
||||
print("Tried to update server cache without a starting state...this is probably an error");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<RemoteServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
|
||||
|
||||
}
|
||||
|
||||
class RemoteServerInfoState extends ChangeNotifier {
|
||||
final String onion;
|
||||
final String status;
|
||||
|
||||
RemoteServerInfoState({required this.onion, required this.status});
|
||||
}
|
|
@ -9,28 +9,72 @@ class ServerListState extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_servers.clear();
|
||||
}
|
||||
|
||||
ServerInfoState? getServer(String onion) {
|
||||
int idx = _servers.indexWhere((element) => element.onion == onion);
|
||||
return idx >= 0 ? _servers[idx] : null;
|
||||
}
|
||||
|
||||
void updateServerCache(String onion, String status) {
|
||||
void add(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
|
||||
var sis = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
|
||||
int idx = _servers.indexWhere((element) => element.onion == onion);
|
||||
if (idx >= 0) {
|
||||
_servers[idx] = ServerInfoState(onion: onion, status: status);
|
||||
_servers[idx] = sis;
|
||||
} else {
|
||||
print("Tried to update server cache without a starting state...this is probably an error");
|
||||
_servers.add(ServerInfoState(onion: onion,
|
||||
serverBundle: serverBundle,
|
||||
running: running,
|
||||
description: description,
|
||||
autoStart: autoStart,
|
||||
isEncrypted: isEncrypted));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateServer(String onion, String serverBundle, bool running, String description, bool autoStart, bool isEncrypted) {
|
||||
int idx = _servers.indexWhere((element) => element.onion == onion);
|
||||
if (idx >= 0) {
|
||||
_servers[idx] = ServerInfoState(onion: onion, serverBundle: serverBundle, running: running, description: description, autoStart: autoStart, isEncrypted: isEncrypted);
|
||||
} else {
|
||||
print("Tried to update server list without a starting state...this is probably an error");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void delete(String onion) {
|
||||
_servers.removeWhere((element) => element.onion == onion);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<ServerInfoState> get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier
|
||||
|
||||
}
|
||||
|
||||
class ServerInfoState extends ChangeNotifier {
|
||||
final String onion;
|
||||
final String status;
|
||||
String onion;
|
||||
String serverBundle;
|
||||
String description;
|
||||
bool running;
|
||||
bool autoStart;
|
||||
bool isEncrypted;
|
||||
|
||||
ServerInfoState({required this.onion, required this.status});
|
||||
ServerInfoState({required this.onion, required this.serverBundle, required this.running, required this.description, required this.autoStart, required this.isEncrypted});
|
||||
|
||||
void setAutostart(bool val) {
|
||||
autoStart = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setRunning(bool val) {
|
||||
running = val;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setDescription(String val) {
|
||||
description = val;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -374,7 +374,7 @@ class OpaqueDark extends OpaqueThemeType {
|
|||
}
|
||||
|
||||
Color defaultButtonDisabledColor() {
|
||||
return deepPurple;
|
||||
return lightGrey;
|
||||
}
|
||||
|
||||
Color defaultButtonDisabledTextColor() {
|
||||
|
@ -630,7 +630,7 @@ class OpaqueLight extends OpaqueThemeType {
|
|||
static final Color whitePurple = Color(0xFFFFFDFF);
|
||||
static final Color softPurple = Color(0xFFFDF3FC);
|
||||
static final Color purple = Color(0xFFDFB9DE);
|
||||
static final Color brightPurple = Color(0xFF760388);
|
||||
static final Color brightPurple = Color(0xFFD1B0E0);
|
||||
static final Color darkPurple = Color(0xFF350052);
|
||||
static final Color greyPurple = Color(0xFF775F84);
|
||||
static final Color pink = Color(0xFFE85DA1);
|
||||
|
@ -684,7 +684,7 @@ class OpaqueLight extends OpaqueThemeType {
|
|||
}
|
||||
|
||||
Color defaultButtonDisabledColor() {
|
||||
return purple;
|
||||
return lightGrey;
|
||||
}
|
||||
|
||||
Color defaultButtonDisabledTextColor() {
|
||||
|
@ -900,11 +900,11 @@ class OpaqueLight extends OpaqueThemeType {
|
|||
}
|
||||
|
||||
Color messageFromMeBackgroundColor() {
|
||||
return darkPurple;
|
||||
return brightPurple;
|
||||
}
|
||||
|
||||
Color messageFromMeTextColor() {
|
||||
return whitePurple;
|
||||
return mainTextColor();
|
||||
}
|
||||
|
||||
Color messageFromOtherBackgroundColor() {
|
||||
|
@ -948,11 +948,14 @@ ThemeData mkThemeData(Settings opaque) {
|
|||
backgroundColor: opaque.current().backgroundMainColor(),
|
||||
highlightColor: opaque.current().hilightElementTextColor(),
|
||||
iconTheme: IconThemeData(
|
||||
color: opaque.current().mainTextColor(),
|
||||
color: opaque.current().toolbarIconColor(),
|
||||
),
|
||||
cardColor: opaque.current().backgroundMainColor(),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: opaque.current().backgroundPaneColor(),
|
||||
iconTheme: IconThemeData(
|
||||
color: opaque.current().mainTextColor(),
|
||||
),
|
||||
titleTextStyle: TextStyle(
|
||||
color: opaque.current().mainTextColor(),
|
||||
),
|
||||
|
@ -969,9 +972,15 @@ ThemeData mkThemeData(Settings opaque) {
|
|||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(opaque.current().defaultButtonColor()),
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) => states.contains(MaterialState.disabled) ? opaque.current().defaultButtonDisabledColor() : opaque.current().defaultButtonColor()),
|
||||
foregroundColor: MaterialStateProperty.all(opaque.current().defaultButtonTextColor()),
|
||||
overlayColor: MaterialStateProperty.all(opaque.current().defaultButtonActiveColor()),
|
||||
overlayColor: MaterialStateProperty.resolveWith((states) => (states.contains(MaterialState.pressed) && states.contains(MaterialState.hovered))
|
||||
? opaque.current().defaultButtonActiveColor()
|
||||
: states.contains(MaterialState.disabled)
|
||||
? opaque.current().defaultButtonDisabledColor()
|
||||
: null),
|
||||
enableFeedback: true,
|
||||
splashFactory: InkRipple.splashFactory,
|
||||
padding: MaterialStateProperty.all(EdgeInsets.all(20)),
|
||||
shape: MaterialStateProperty.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
|
@ -1004,7 +1013,11 @@ ThemeData mkThemeData(Settings opaque) {
|
|||
thumbColor: MaterialStateProperty.all(opaque.current().mainTextColor()),
|
||||
trackColor: MaterialStateProperty.all(opaque.current().dropShadowColor()),
|
||||
),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(backgroundColor: opaque.current().defaultButtonColor(), hoverColor: opaque.current().defaultButtonActiveColor()),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: opaque.current().defaultButtonColor(),
|
||||
hoverColor: opaque.current().defaultButtonActiveColor(),
|
||||
enableFeedback: true,
|
||||
splashColor: opaque.current().defaultButtonActiveColor()),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: opaque.current().defaultButtonActiveColor(), selectionColor: opaque.current().defaultButtonActiveColor(), selectionHandleColor: opaque.current().defaultButtonActiveColor()),
|
||||
);
|
||||
|
|
|
@ -9,6 +9,9 @@ import 'opaque.dart';
|
|||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
const TapirGroupsExperiment = "tapir-groups-experiment";
|
||||
const ServerManagementExperiment = "servers-experiment";
|
||||
const FileSharingExperiment = "filesharing";
|
||||
const ClickableLinksExperiment = "clickable-links";
|
||||
|
||||
enum DualpaneMode {
|
||||
Single,
|
||||
|
@ -31,6 +34,7 @@ class Settings extends ChangeNotifier {
|
|||
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
|
||||
|
||||
bool blockUnknownConnections = false;
|
||||
bool streamerMode = false;
|
||||
|
||||
/// Set the dark theme.
|
||||
void setDark() {
|
||||
|
@ -74,11 +78,11 @@ class Settings extends ChangeNotifier {
|
|||
// Set Locale and notify listeners
|
||||
switchLocale(Locale(settings["Locale"]));
|
||||
|
||||
// Decide whether to enable Experiments
|
||||
blockUnknownConnections = settings["BlockUnknownConnections"];
|
||||
blockUnknownConnections = settings["BlockUnknownConnections"] ?? false;
|
||||
streamerMode = settings["StreamerMode"] ?? false;
|
||||
|
||||
// Decide whether to enable Experiments
|
||||
experimentsEnabled = settings["ExperimentsEnabled"];
|
||||
experimentsEnabled = settings["ExperimentsEnabled"] ?? false;
|
||||
|
||||
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
|
||||
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
|
||||
|
@ -105,6 +109,11 @@ class Settings extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
setStreamerMode(bool newSteamerMode) {
|
||||
streamerMode = newSteamerMode;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Block Unknown Connections will autoblock connections if they authenticate with public key not in our contacts list.
|
||||
/// This is one of the best tools we have to combat abuse, while it isn't ideal it does allow a user to curate their contacts
|
||||
/// list without being bothered by spurious requests (either permanently, or as a short term measure).
|
||||
|
@ -227,6 +236,7 @@ class Settings extends ChangeNotifier {
|
|||
"Theme": themeString,
|
||||
"PreviousPid": -1,
|
||||
"BlockUnknownConnections": blockUnknownConnections,
|
||||
"StreamerMode": streamerMode,
|
||||
"ExperimentsEnabled": this.experimentsEnabled,
|
||||
"Experiments": experiments,
|
||||
"StateRootPane": 0,
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:cwtch/cwtch_icons_icons.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cwtch/errorHandler.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/models/profileservers.dart';
|
||||
import 'package:cwtch/settings.dart';
|
||||
import 'package:cwtch/widgets/buttontextfield.dart';
|
||||
import 'package:cwtch/widgets/cwtchlabel.dart';
|
||||
|
@ -197,7 +197,7 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
},
|
||||
isExpanded: true, // magic property
|
||||
value: server,
|
||||
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((ServerInfoState serverInfo) {
|
||||
items: Provider.of<ProfileInfoState>(context).serverList.servers.map<DropdownMenuItem<String>>((RemoteServerInfoState serverInfo) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: serverInfo.onion,
|
||||
child: Text(
|
||||
|
@ -240,8 +240,8 @@ class _AddContactViewState extends State<AddContactView> {
|
|||
|
||||
/// TODO Manage Servers Tab
|
||||
Widget manageServersTab() {
|
||||
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((ServerInfoState server) {
|
||||
return ChangeNotifierProvider<ServerInfoState>.value(
|
||||
final tiles = Provider.of<ProfileInfoState>(context).serverList.servers.map((RemoteServerInfoState server) {
|
||||
return ChangeNotifierProvider<RemoteServerInfoState>.value(
|
||||
value: server,
|
||||
child: ListTile(
|
||||
title: Text(server.onion),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cwtch/cwtch/cwtch.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:cwtch/model.dart';
|
||||
|
@ -106,6 +107,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
labelText: AppLocalizations.of(context)!.yourDisplayName,
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
// TODO l10n ize
|
||||
return "Please enter a display name";
|
||||
}
|
||||
return null;
|
||||
|
@ -263,7 +265,6 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
|
||||
icon: Icon(Icons.delete_forever),
|
||||
label: Text(AppLocalizations.of(context)!.deleteBtn),
|
||||
))
|
||||
|
@ -288,32 +289,19 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, "be gay do crime");
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, DefaultPassword);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} else {
|
||||
// Profile Editing
|
||||
if (ctrlrPass.value.text.isEmpty) {
|
||||
// Don't update password, only update name
|
||||
final event = {
|
||||
"EventType": "SetAttribute",
|
||||
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
|
||||
};
|
||||
final json = jsonEncode(event);
|
||||
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, json);
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// At this points passwords have been validated to be the same and not empty
|
||||
// Update both password and name, even if name hasn't been changed...
|
||||
final updateNameEvent = {
|
||||
"EventType": "SetAttribute",
|
||||
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
|
||||
};
|
||||
final updateNameEventJson = jsonEncode(updateNameEvent);
|
||||
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updateNameEventJson);
|
||||
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetProfileAttribute(Provider.of<ProfileInfoState>(context, listen: false).onion, "profile.name", ctrlrNick.value.text);
|
||||
final updatePasswordEvent = {
|
||||
"EventType": "ChangePassword",
|
||||
"Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text}
|
||||
|
@ -330,13 +318,13 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
|
|||
|
||||
showAlertDialog(BuildContext context) {
|
||||
// set up the buttons
|
||||
Widget cancelButton = TextButton(
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = TextButton(
|
||||
Widget continueButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.deleteProfileConfirmBtn),
|
||||
onPressed: () {
|
||||
var onion = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
|
|
|
@ -0,0 +1,398 @@
|
|||
import 'dart:convert';
|
||||
import 'package:cwtch/cwtch/cwtch.dart';
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/widgets/cwtchlabel.dart';
|
||||
import 'package:cwtch/widgets/passwordfield.dart';
|
||||
import 'package:cwtch/widgets/textfield.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/settings.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../errorHandler.dart';
|
||||
import '../main.dart';
|
||||
import '../config.dart';
|
||||
|
||||
/// Pane to add or edit a server
|
||||
class AddEditServerView extends StatefulWidget {
|
||||
const AddEditServerView();
|
||||
|
||||
@override
|
||||
_AddEditServerViewState createState() => _AddEditServerViewState();
|
||||
}
|
||||
|
||||
class _AddEditServerViewState extends State<AddEditServerView> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final ctrlrDesc = TextEditingController(text: "");
|
||||
final ctrlrOldPass = TextEditingController(text: "");
|
||||
final ctrlrPass = TextEditingController(text: "");
|
||||
final ctrlrPass2 = TextEditingController(text: "");
|
||||
final ctrlrOnion = TextEditingController(text: "");
|
||||
|
||||
late bool usePassword;
|
||||
//late bool deleted;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var serverInfoState = Provider.of<ServerInfoState>(context, listen: false);
|
||||
ctrlrOnion.text = serverInfoState.onion;
|
||||
usePassword = serverInfoState.isEncrypted;
|
||||
if (serverInfoState.description.isNotEmpty) {
|
||||
ctrlrDesc.text = serverInfoState.description;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: ctrlrOnion.text.isEmpty ? Text(AppLocalizations.of(context)!.addServerTitle) : Text(AppLocalizations.of(context)!.editServerTitle),
|
||||
),
|
||||
body: _buildSettingsList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSwitchPassword(bool? value) {
|
||||
setState(() {
|
||||
usePassword = value!;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSettingsList() {
|
||||
return Consumer2<ServerInfoState, Settings>(builder: (context, serverInfoState, settings, child) {
|
||||
return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) {
|
||||
return Scrollbar(
|
||||
isAlwaysShown: true,
|
||||
child: SingleChildScrollView(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
margin: EdgeInsets.fromLTRB(30, 0, 30, 10),
|
||||
padding: EdgeInsets.fromLTRB(20, 0 , 20, 10),
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
||||
// Onion
|
||||
Visibility(
|
||||
visible: serverInfoState.onion.isNotEmpty,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.serverAddress),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
SelectableText(
|
||||
serverInfoState.onion
|
||||
)
|
||||
])),
|
||||
|
||||
// Description
|
||||
Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel),
|
||||
Text(AppLocalizations.of(context)!.serverDescriptionDescription),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchTextField(
|
||||
controller: ctrlrDesc,
|
||||
labelText: "Description",
|
||||
autofocus: false,
|
||||
)
|
||||
]),
|
||||
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
|
||||
// Enabled
|
||||
Visibility(
|
||||
visible: serverInfoState.onion.isNotEmpty,
|
||||
child: SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.serverEnabled, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.serverEnabledDescription),
|
||||
value: serverInfoState.running,
|
||||
onChanged: (bool value) {
|
||||
serverInfoState.setRunning(value);
|
||||
if (value) {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.LaunchServer(serverInfoState.onion);
|
||||
} else {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.StopServer(serverInfoState.onion);
|
||||
}
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()),
|
||||
)),
|
||||
|
||||
// Auto start
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.serverAutostartLabel, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.serverAutostartDescription),
|
||||
value: serverInfoState.autoStart,
|
||||
onChanged: (bool value) {
|
||||
serverInfoState.setAutostart(value);
|
||||
|
||||
if (! serverInfoState.onion.isEmpty) {
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false");
|
||||
}
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()),
|
||||
),
|
||||
|
||||
|
||||
// ***** Password *****
|
||||
|
||||
// use password toggle
|
||||
Visibility(
|
||||
visible: serverInfoState.onion.isEmpty,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Checkbox(
|
||||
value: usePassword,
|
||||
fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()),
|
||||
activeColor: settings.current().defaultButtonActiveColor(),
|
||||
onChanged: _handleSwitchPassword,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.radioUsePassword,
|
||||
style: TextStyle(color: settings.current().mainTextColor()),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Text(
|
||||
usePassword ? AppLocalizations.of(context)!.encryptedServerDescription : AppLocalizations.of(context)!.plainServerDescription,
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
])),
|
||||
|
||||
|
||||
// current password
|
||||
Visibility(
|
||||
visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
controller: ctrlrOldPass,
|
||||
autoFillHints: [AutofillHints.newPassword],
|
||||
validator: (value) {
|
||||
// Password field can be empty when just updating the profile, not on creation
|
||||
if (serverInfoState.isEncrypted &&
|
||||
serverInfoState.onion.isEmpty &&
|
||||
value.isEmpty &&
|
||||
usePassword) {
|
||||
return AppLocalizations.of(context)!.passwordErrorEmpty;
|
||||
}
|
||||
if (Provider.of<ErrorHandler>(context).deletedServerError == true) {
|
||||
return AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
])),
|
||||
|
||||
// new passwords 1 & 2
|
||||
Visibility(
|
||||
// Currently we don't support password change for servers so also gate this on Add server, when ready to support changing password remove the onion.isEmpty check
|
||||
visible: serverInfoState.onion.isEmpty && usePassword,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.newPassword),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
controller: ctrlrPass,
|
||||
validator: (value) {
|
||||
// Password field can be empty when just updating the profile, not on creation
|
||||
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
|
||||
return AppLocalizations.of(context)!.passwordErrorEmpty;
|
||||
}
|
||||
if (value != ctrlrPass2.value.text) {
|
||||
return AppLocalizations.of(context)!.passwordErrorMatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchLabel(label: AppLocalizations.of(context)!.password2Label),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
controller: ctrlrPass2,
|
||||
validator: (value) {
|
||||
// Password field can be empty when just updating the profile, not on creation
|
||||
if (serverInfoState.onion.isEmpty && value.isEmpty && usePassword) {
|
||||
return AppLocalizations.of(context)!.passwordErrorEmpty;
|
||||
}
|
||||
if (value != ctrlrPass.value.text) {
|
||||
return AppLocalizations.of(context)!.passwordErrorMatch;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
]),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed,
|
||||
child: Text(
|
||||
serverInfoState.onion.isEmpty ? AppLocalizations.of(context)!.addServerTitle : AppLocalizations.of(context)!.saveServerButton,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: serverInfoState.onion.isNotEmpty,
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Tooltip(
|
||||
message: AppLocalizations.of(context)!.enterCurrentPasswordForDeleteServer,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
},
|
||||
icon: Icon(Icons.delete_forever),
|
||||
label: Text(AppLocalizations.of(context)!.deleteBtn),
|
||||
))
|
||||
]))
|
||||
|
||||
// ***** END Password *****
|
||||
|
||||
]))))));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _createPressed() {
|
||||
// This will run all the validations in the form including
|
||||
// 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).
|
||||
if (_formKey.currentState!.validate()) {
|
||||
if (usePassword) {
|
||||
Provider
|
||||
.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
|
||||
} else {
|
||||
Provider
|
||||
.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of<ServerInfoState>(context, listen: false).autoStart);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _savePressed() {
|
||||
|
||||
var server = Provider.of<ServerInfoState>(context, listen: false);
|
||||
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text);
|
||||
server.setDescription(ctrlrDesc.text);
|
||||
|
||||
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// TODO support change password
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
showAlertDialog(BuildContext context) {
|
||||
// set up the buttons
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
|
||||
onPressed: () {
|
||||
var onion = Provider
|
||||
.of<ServerInfoState>(context, listen: false)
|
||||
.onion;
|
||||
Provider
|
||||
.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.DeleteServer(onion, Provider.of<ServerInfoState>(context, listen: false).isEncrypted ? ctrlrOldPass.value.text : DefaultPassword);
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
if (globalErrorHandler.deletedServerSuccess) {
|
||||
final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.deleteServerSuccess + ":" + onion));
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
Navigator.of(context).popUntil((route) => route.settings.name == "servers"); // dismiss dialog
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
// set up the AlertDialog
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteServerConfirmBtn),
|
||||
actions: [
|
||||
cancelButton,
|
||||
continueButton,
|
||||
],
|
||||
);
|
||||
|
||||
// show the dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import 'package:cwtch/views/torstatusview.dart';
|
|||
import 'package:cwtch/widgets/contactrow.dart';
|
||||
import 'package:cwtch/widgets/profileimage.dart';
|
||||
import 'package:cwtch/widgets/textfield.dart';
|
||||
import 'package:cwtch/widgets/tor_icon.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../main.dart';
|
||||
import '../settings.dart';
|
||||
|
@ -23,18 +23,20 @@ class ContactsView extends StatefulWidget {
|
|||
|
||||
// selectConversation can be called from anywhere to set the active conversation
|
||||
void selectConversation(BuildContext context, String handle) {
|
||||
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
|
||||
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
|
||||
var initialIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
|
||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
|
||||
// triggers update in Double/TripleColumnView
|
||||
Provider.of<AppState>(context, listen: false).initialScrollIndex = initialIndex;
|
||||
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
|
||||
// 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(context, handle, initialIndex);
|
||||
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
|
||||
}
|
||||
|
||||
void _pushMessageView(BuildContext context, String handle, int initialIndex) {
|
||||
void _pushMessageView(BuildContext context, String handle) {
|
||||
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
|
@ -47,7 +49,7 @@ void _pushMessageView(BuildContext context, String handle, int initialIndex) {
|
|||
ChangeNotifierProvider.value(value: profile),
|
||||
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
|
||||
],
|
||||
builder: (context, child) => MessageView(initialIndex),
|
||||
builder: (context, child) => MessageView(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -86,18 +88,7 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
child: Text("%1 » %2".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname).replaceAll("%2", AppLocalizations.of(context)!.titleManageContacts),
|
||||
overflow: TextOverflow.ellipsis, style: TextStyle(color: Provider.of<Settings>(context).current().mainTextColor()))),
|
||||
])),
|
||||
actions: [
|
||||
IconButton(icon: TorIcon(), onPressed: _pushTorStatus),
|
||||
IconButton(
|
||||
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
|
||||
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
|
||||
onPressed: () {
|
||||
Provider.of<ContactListState>(context, listen: false).filter = "";
|
||||
setState(() {
|
||||
showSearchBar = !showSearchBar;
|
||||
});
|
||||
})
|
||||
],
|
||||
actions: getActions(context),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _pushAddContact,
|
||||
|
@ -107,6 +98,36 @@ class _ContactsViewState extends State<ContactsView> {
|
|||
body: showSearchBar || Provider.of<ContactListState>(context).isFiltered ? _buildFilterable() : _buildContactList());
|
||||
}
|
||||
|
||||
List<Widget> getActions(context) {
|
||||
var actions = List<Widget>.empty(growable: true);
|
||||
if (Provider.of<Settings>(context).blockUnknownConnections) {
|
||||
actions.add(Tooltip(message: AppLocalizations.of(context)!.blockUnknownConnectionsEnabledDescription, child: Icon(CwtchIcons.block_unknown)));
|
||||
}
|
||||
|
||||
// Copy profile onion
|
||||
actions.add(IconButton(
|
||||
icon: Icon(CwtchIcons.address_copy_2),
|
||||
tooltip: AppLocalizations.of(context)!.copyAddress,
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
|
||||
}));
|
||||
|
||||
|
||||
// TODO servers
|
||||
|
||||
// Search contacts
|
||||
actions.add(IconButton(
|
||||
// need both conditions for displaying initial empty textfield and also allowing filters to be cleared if this widget gets lost/reset
|
||||
icon: Icon(showSearchBar || Provider.of<ContactListState>(context).isFiltered ? Icons.search_off : Icons.search),
|
||||
onPressed: () {
|
||||
Provider.of<ContactListState>(context, listen: false).filter = "";
|
||||
setState(() {
|
||||
showSearchBar = !showSearchBar;
|
||||
});
|
||||
}));
|
||||
return actions;
|
||||
}
|
||||
|
||||
Widget _buildFilterable() {
|
||||
Widget txtfield = CwtchTextField(
|
||||
controller: ctrlrFilter,
|
||||
|
|
|
@ -17,7 +17,6 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
|
|||
Widget build(BuildContext context) {
|
||||
var flwtch = Provider.of<AppState>(context);
|
||||
var cols = Provider.of<Settings>(context).uiColumns(true);
|
||||
var initialIndex = flwtch.selectedConversation == null ? 0 : Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(flwtch.selectedConversation!)!.unreadMessages;
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
|
@ -36,7 +35,7 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
|
|||
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
|
||||
ChangeNotifierProvider.value(
|
||||
value: flwtch.selectedConversation != null ? Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)! : ContactInfoState("", "")),
|
||||
], child: Container(child: MessageView(initialIndex))),
|
||||
], child: Container(key: Key(flwtch.selectedConversation??"never_this"), child: MessageView())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/settings.dart';
|
||||
|
@ -137,6 +139,19 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.block_unknown, color: settings.current().mainTextColor()),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.streamerModeLabel, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.descriptionStreamerMode),
|
||||
value: settings.streamerMode,
|
||||
onChanged: (bool value) {
|
||||
settings.setStreamerMode(value);
|
||||
// Save Settings...
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.streamer_bunnymask, color: settings.current().mainTextColor()),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.experimentsEnabled, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.descriptionExperiments),
|
||||
|
@ -175,6 +190,59 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
|
|||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.enable_groups, color: settings.current().mainTextColor()),
|
||||
),
|
||||
Visibility(
|
||||
visible: !Platform.isAndroid && !Platform.isIOS,
|
||||
child:
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.settingServers, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.settingServersDescription),
|
||||
value: settings.isExperimentEnabled(ServerManagementExperiment),
|
||||
onChanged: (bool value) {
|
||||
Provider.of<ServerListState>(context, listen: false).clear();
|
||||
if (value) {
|
||||
settings.enableExperiment(ServerManagementExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(ServerManagementExperiment);
|
||||
}
|
||||
// Save Settings...
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(CwtchIcons.dns_24px, color: settings.current().mainTextColor()),
|
||||
)),
|
||||
SwitchListTile(
|
||||
title: Text(AppLocalizations.of(context)!.settingFileSharing, style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text(AppLocalizations.of(context)!.descriptionFileSharing),
|
||||
value: settings.isExperimentEnabled(FileSharingExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(FileSharingExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(FileSharingExperiment);
|
||||
}
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())),
|
||||
subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."),
|
||||
value: settings.isExperimentEnabled(ClickableLinksExperiment),
|
||||
onChanged: (bool value) {
|
||||
if (value) {
|
||||
settings.enableExperiment(ClickableLinksExperiment);
|
||||
} else {
|
||||
settings.disableExperiment(ClickableLinksExperiment);
|
||||
}
|
||||
saveSettings(context);
|
||||
},
|
||||
activeTrackColor: settings.theme.defaultButtonActiveColor(),
|
||||
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
|
||||
secondary: Icon(Icons.link, color: settings.current().mainTextColor()),
|
||||
),
|
||||
],
|
||||
)),
|
||||
AboutListTile(
|
||||
|
@ -227,6 +295,9 @@ String getLanguageFull(context, String languageCode) {
|
|||
if (languageCode == "pl") {
|
||||
return AppLocalizations.of(context)!.localePl;
|
||||
}
|
||||
if (languageCode == "ru") {
|
||||
return AppLocalizations.of(context)!.localeRU;
|
||||
}
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
|
|
|
@ -136,14 +136,40 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
|
|||
height: 20,
|
||||
),
|
||||
Tooltip(
|
||||
message: AppLocalizations.of(context)!.leaveGroup,
|
||||
message: AppLocalizations.of(context)!.archiveConversation,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||
// locally update cache...
|
||||
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
Provider.of<AppState>(context, listen: false).selectedConversation = null;
|
||||
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
|
||||
});
|
||||
},
|
||||
icon: Icon(CwtchIcons.leave_group),
|
||||
label: Text(AppLocalizations.of(context)!.leaveGroup),
|
||||
))
|
||||
icon: Icon(CwtchIcons.leave_chat),
|
||||
label: Text(AppLocalizations.of(context)!.archiveConversation),
|
||||
)),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Tooltip(
|
||||
message: AppLocalizations.of(context)!.leaveGroup,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
},
|
||||
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
|
||||
icon: Icon(CwtchIcons.leave_group),
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.leaveGroup,
|
||||
style: TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
))
|
||||
])
|
||||
])
|
||||
])))));
|
||||
});
|
||||
|
@ -158,20 +184,21 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
|
|||
|
||||
showAlertDialog(BuildContext context) {
|
||||
// set up the buttons
|
||||
Widget cancelButton = TextButton(
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = TextButton(
|
||||
Widget continueButton = ElevatedButton(
|
||||
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);
|
||||
// locally update cache...
|
||||
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
Provider.of<AppState>(context, listen: false).selectedConversation = null;
|
||||
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
|
||||
|
|
|
@ -6,6 +6,9 @@ import 'package:cwtch/models/message.dart';
|
|||
import 'package:cwtch/models/messages/quotedmessage.dart';
|
||||
import 'package:cwtch/widgets/messageloadingbubble.dart';
|
||||
import 'package:cwtch/widgets/profileimage.dart';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/views/peersettingsview.dart';
|
||||
|
@ -14,6 +17,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:path/path.dart' show basename;
|
||||
|
||||
import '../main.dart';
|
||||
import '../model.dart';
|
||||
|
@ -22,9 +26,6 @@ import '../widgets/messagelist.dart';
|
|||
import 'groupsettingsview.dart';
|
||||
|
||||
class MessageView extends StatefulWidget {
|
||||
int initialIndex;
|
||||
MessageView(this.initialIndex);
|
||||
|
||||
@override
|
||||
_MessageViewState createState() => _MessageViewState();
|
||||
}
|
||||
|
@ -38,24 +39,30 @@ class _MessageViewState extends State<MessageView> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
// using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
|
||||
if (widget.initialIndex > 8) {
|
||||
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
|
||||
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = true;
|
||||
});
|
||||
}
|
||||
|
||||
scrollListener.itemPositions.addListener(() {
|
||||
var first = scrollListener.itemPositions.value.first.index;
|
||||
var last = scrollListener.itemPositions.value.last.index;
|
||||
// sometimes these go hi->lo and sometimes they go lo->hi because [who tf knows]
|
||||
if (first == 0 || last == 0) {
|
||||
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
|
||||
if (scrollListener.itemPositions.value.length != 0 &&
|
||||
Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
|
||||
scrollListener.itemPositions.value.any((element) => element.index == 0)) {
|
||||
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
|
||||
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
var appState = Provider.of<AppState>(context, listen: false);
|
||||
|
||||
// using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
|
||||
if (appState.initialScrollIndex > 4 && appState.unreadMessagesBelow == false) {
|
||||
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
|
||||
appState.unreadMessagesBelow = true;
|
||||
});
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
|
@ -70,13 +77,41 @@ class _MessageViewState extends State<MessageView> {
|
|||
return Card(child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)));
|
||||
}
|
||||
|
||||
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||
var appBarButtons = <Widget>[];
|
||||
if (Provider.of<ContactInfoState>(context).isOnline()) {
|
||||
if (showFileSharing) {
|
||||
appBarButtons.add(IconButton(
|
||||
icon: Icon(Icons.attach_file, size: 24),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipSendFile,
|
||||
onPressed: _showFilePicker,
|
||||
));
|
||||
}
|
||||
appBarButtons.add(IconButton(
|
||||
icon: Icon(CwtchIcons.send_invite, size: 24),
|
||||
tooltip: AppLocalizations.of(context)!.sendInvite,
|
||||
onPressed: () {
|
||||
_modalSendInvitation(context);
|
||||
}));
|
||||
}
|
||||
appBarButtons.add(IconButton(
|
||||
icon: Provider.of<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px),
|
||||
tooltip: AppLocalizations.of(context)!.conversationSettings,
|
||||
onPressed: _pushContactSettings));
|
||||
|
||||
var appState = Provider.of<AppState>(context);
|
||||
return WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
child: Scaffold(
|
||||
floatingActionButton: appState.unreadMessagesBelow ? FloatingActionButton(child: Icon(Icons.arrow_downward), onPressed: (){
|
||||
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
|
||||
}) : null,
|
||||
floatingActionButton: appState.unreadMessagesBelow
|
||||
? FloatingActionButton(
|
||||
child: Icon(Icons.arrow_downward),
|
||||
onPressed: () {
|
||||
Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
|
||||
Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
|
||||
scrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
|
||||
})
|
||||
: null,
|
||||
appBar: AppBar(
|
||||
// setting leading to null makes it do the default behaviour; container() hides it
|
||||
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
|
||||
|
@ -97,17 +132,9 @@ class _MessageViewState extends State<MessageView> {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
))
|
||||
]),
|
||||
actions: [
|
||||
//IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings),
|
||||
//IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings),
|
||||
//IconButton(icon: Icon(Icons.push_pin), onPressed: _pushContactSettings),
|
||||
IconButton(
|
||||
icon: Provider.of<ContactInfoState>(context, listen: false).isGroup == true ? Icon(CwtchIcons.group_settings_24px) : Icon(CwtchIcons.peer_settings_24px),
|
||||
tooltip: AppLocalizations.of(context)!.conversationSettings,
|
||||
onPressed: _pushContactSettings),
|
||||
],
|
||||
actions: appBarButtons,
|
||||
),
|
||||
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(widget.initialIndex, scrollController, scrollListener)),
|
||||
body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 108.0), child: MessageList(scrollController, scrollListener)),
|
||||
bottomSheet: _buildComposeBox(),
|
||||
));
|
||||
}
|
||||
|
@ -175,11 +202,19 @@ class _MessageViewState extends State<MessageView> {
|
|||
_sendMessageHelper();
|
||||
}
|
||||
|
||||
void _sendFile(String filePath) {
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, filePath);
|
||||
_sendMessageHelper();
|
||||
}
|
||||
|
||||
void _sendMessageHelper() {
|
||||
ctrlrCompose.clear();
|
||||
focusNode.requestFocus();
|
||||
Future.delayed(const Duration(milliseconds: 80), () {
|
||||
Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
|
||||
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
|
||||
// Resort the contact list...
|
||||
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).onion, DateTime.now());
|
||||
});
|
||||
|
@ -196,42 +231,36 @@ class _MessageViewState extends State<MessageView> {
|
|||
child: Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))),
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: handleKeyPress,
|
||||
child: TextFormField(
|
||||
key: Key('txtCompose'),
|
||||
controller: ctrlrCompose,
|
||||
focusNode: focusNode,
|
||||
autofocus: !Platform.isAndroid,
|
||||
textInputAction: TextInputAction.newline,
|
||||
keyboardType: TextInputType.multiline,
|
||||
minLines: 1,
|
||||
maxLines: null,
|
||||
onFieldSubmitted: _sendMessage,
|
||||
enabled: !isOffline,
|
||||
decoration: InputDecoration(
|
||||
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
|
||||
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.altTextColor()),
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabled: true,
|
||||
prefixIcon: IconButton(
|
||||
icon: Icon(CwtchIcons.send_invite, size: 24, color: Provider.of<Settings>(context).theme.mainTextColor()),
|
||||
tooltip: AppLocalizations.of(context)!.sendInvite,
|
||||
enableFeedback: true,
|
||||
splashColor: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
|
||||
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: isOffline ? null : _sendMessage,
|
||||
),
|
||||
)))),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor()))),
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: handleKeyPress,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: TextFormField(
|
||||
key: Key('txtCompose'),
|
||||
controller: ctrlrCompose,
|
||||
focusNode: focusNode,
|
||||
autofocus: !Platform.isAndroid,
|
||||
textInputAction: TextInputAction.newline,
|
||||
keyboardType: TextInputType.multiline,
|
||||
enableIMEPersonalizedLearning: false,
|
||||
minLines: 1,
|
||||
maxLines: null,
|
||||
onFieldSubmitted: _sendMessage,
|
||||
enabled: !isOffline,
|
||||
decoration: InputDecoration(
|
||||
hintText: isOffline ? "" : AppLocalizations.of(context)!.placeholderEnterMessage,
|
||||
hintStyle: TextStyle(color: Provider.of<Settings>(context).theme.altTextColor()),
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabled: true,
|
||||
suffixIcon: ElevatedButton(
|
||||
child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor()),
|
||||
onPressed: isOffline ? null : _sendMessage,
|
||||
))),
|
||||
)))),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -249,18 +278,27 @@ class _MessageViewState extends State<MessageView> {
|
|||
color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
|
||||
? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor()
|
||||
: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
|
||||
child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(Icons.reply, size: 32))),
|
||||
Center(widthFactor: 1.0, child: message.getPreviewWidget(context)),
|
||||
Center(
|
||||
widthFactor: 1.0,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.highlight_remove),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
|
||||
onPressed: () {
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
},
|
||||
))
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Stack(children: [
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.highlight_remove),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
|
||||
onPressed: () {
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
},
|
||||
)),
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(padding: EdgeInsets.all(2.0), child: Icon(Icons.reply)),
|
||||
)
|
||||
]),
|
||||
Wrap(
|
||||
runAlignment: WrapAlignment.spaceEvenly,
|
||||
alignment: WrapAlignment.center,
|
||||
runSpacing: 1.0,
|
||||
children: [Center(widthFactor: 1.0, child: Padding(padding: EdgeInsets.all(10.0), child: message.getPreviewWidget(context)))]),
|
||||
]));
|
||||
} else {
|
||||
return MessageLoadingBubble();
|
||||
|
@ -334,4 +372,20 @@ class _MessageViewState extends State<MessageView> {
|
|||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _showFilePicker() async {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
if (result != null) {
|
||||
File file = File(result.files.first.path);
|
||||
// We have a maximum number of bytes we can represent in terms of
|
||||
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
|
||||
if (file.lengthSync() <= 10737418240) {
|
||||
print("Sending " + file.path);
|
||||
_sendFile(file.path);
|
||||
} else {
|
||||
print("file size cannot exceed 10 gigabytes");
|
||||
//todo: toast error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,13 +196,21 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
|
|||
),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Tooltip(
|
||||
message: AppLocalizations.of(context)!.leaveGroup,
|
||||
message: AppLocalizations.of(context)!.archiveConversation,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showAlertDialog(context);
|
||||
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||
var handle = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||
// locally update cache...
|
||||
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.ArchiveConversation(profileOnion, handle);
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
Provider.of<AppState>(context, listen: false).selectedConversation = null;
|
||||
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
|
||||
});
|
||||
},
|
||||
icon: Icon(CwtchIcons.leave_chat),
|
||||
label: Text(AppLocalizations.of(context)!.leaveGroup),
|
||||
label: Text(AppLocalizations.of(context)!.archiveConversation),
|
||||
))
|
||||
])
|
||||
]),
|
||||
|
@ -226,13 +234,15 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
|
|||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = TextButton(
|
||||
Widget continueButton = ElevatedButton(
|
||||
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);
|
||||
// locally update cache...
|
||||
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
Provider.of<AppState>(context, listen: false).selectedConversation = null;
|
||||
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
|
||||
|
|
|
@ -17,6 +17,7 @@ import '../model.dart';
|
|||
import '../torstatus.dart';
|
||||
import 'addeditprofileview.dart';
|
||||
import 'globalsettingsview.dart';
|
||||
import 'serversview.dart';
|
||||
|
||||
class ProfileMgrView extends StatefulWidget {
|
||||
ProfileMgrView();
|
||||
|
@ -51,16 +52,19 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
Icon(
|
||||
CwtchIcons.cwtch_knott,
|
||||
size: 36,
|
||||
color: settings.theme.mainTextColor(),
|
||||
),
|
||||
SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
|
||||
Expanded(child: Text(MediaQuery.of(context).size.width > 600 ?
|
||||
AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
|
||||
style: TextStyle(color: settings.current().mainTextColor())))
|
||||
]),
|
||||
actions: getActions(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _pushAddEditProfile,
|
||||
onPressed: _pushAddProfile,
|
||||
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
|
@ -94,6 +98,11 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
onPressed: _modalUnlockProfiles,
|
||||
));
|
||||
|
||||
// Servers
|
||||
if (Provider.of<Settings>(context).isExperimentEnabled(ServerManagementExperiment) && !Platform.isAndroid && !Platform.isIOS) {
|
||||
actions.add(IconButton(icon: Icon(CwtchIcons.dns_black_24dp), tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers));
|
||||
}
|
||||
|
||||
// Global Settings
|
||||
actions.add(IconButton(icon: Icon(Icons.settings), tooltip: AppLocalizations.of(context)!.tooltipOpenSettings, onPressed: _pushGlobalSettings));
|
||||
|
||||
|
@ -118,6 +127,18 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
));
|
||||
}
|
||||
|
||||
void _pushServers() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
settings: RouteSettings(name: "servers"),
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
|
||||
child: ServersView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
void _pushTorStatus() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -129,7 +150,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
));
|
||||
}
|
||||
|
||||
void _pushAddEditProfile({onion: ""}) {
|
||||
void _pushAddProfile({onion: ""}) {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
|
@ -215,9 +236,9 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
|
|||
).toList();
|
||||
|
||||
if (tiles.isEmpty) {
|
||||
return const Center(
|
||||
child: const Text(
|
||||
"Please create or unlock a profile to begin!",
|
||||
return Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.unlockProfileTip,
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/views/addeditservers.dart';
|
||||
import 'package:cwtch/widgets/passwordfield.dart';
|
||||
import 'package:cwtch/widgets/serverrow.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/torstatus.dart';
|
||||
import 'package:cwtch/widgets/tor_icon.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../cwtch_icons_icons.dart';
|
||||
import '../main.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
///
|
||||
class ServersView extends StatefulWidget {
|
||||
@override
|
||||
_ServersView createState() => _ServersView();
|
||||
}
|
||||
|
||||
class _ServersView extends State<ServersView> {
|
||||
final ctrlrPassword = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ctrlrPassword.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort),
|
||||
actions: getActions(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _pushAddServer,
|
||||
tooltip: AppLocalizations.of(context)!.addServerTooltip,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
semanticLabel: AppLocalizations.of(context)!.addServerTooltip,
|
||||
),
|
||||
),
|
||||
body: Consumer<ServerListState>(
|
||||
builder: (context, svrs, child) {
|
||||
final tiles = svrs.servers.map((ServerInfoState server) {
|
||||
return ChangeNotifierProvider<ServerInfoState>.value(
|
||||
value: server,
|
||||
builder: (context, child) => RepaintBoundary(child: ServerRow()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final divided = ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: tiles,
|
||||
).toList();
|
||||
|
||||
if (tiles.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.unlockServerTip,
|
||||
textAlign: TextAlign.center,
|
||||
));
|
||||
}
|
||||
|
||||
return ListView(children: divided);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
List<Widget> getActions() {
|
||||
List<Widget> actions = new List<Widget>.empty(growable: true);
|
||||
|
||||
// Unlock Profiles
|
||||
actions.add(IconButton(
|
||||
icon: Icon(CwtchIcons.lock_open_24px),
|
||||
color: Provider.of<ServerListState>(context).servers.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor() : Provider.of<Settings>(context).theme.mainTextColor(),
|
||||
tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
|
||||
onPressed: _modalUnlockServers,
|
||||
));
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
void _modalUnlockServers() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: RepaintBoundary(
|
||||
child: Container(
|
||||
height: 200, // bespoke value courtesy of the [TextField] docs
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(AppLocalizations.of(context)!.enterServerPassword),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CwtchPasswordField(
|
||||
autofocus: true,
|
||||
controller: ctrlrPassword,
|
||||
action: unlock,
|
||||
validator: (value) {},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
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.LoadServers(password);
|
||||
ctrlrPassword.text = "";
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _pushAddServer() {
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider<ServerInfoState>(
|
||||
create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true),
|
||||
)],
|
||||
child: AddEditServerView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ class _TripleColumnViewState extends State<TripleColumnView> {
|
|||
child: appState.selectedConversation == null
|
||||
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
|
||||
: //dev
|
||||
Container(child: MessageView(0/*todo:setme*/)),
|
||||
Container(child: MessageView()),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
|
|||
readOnly: widget.readonly,
|
||||
showCursor: !widget.readonly,
|
||||
focusNode: _focusNode,
|
||||
enableIMEPersonalizedLearning: false,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
onPressed: widget.onPressed,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/views/contactsview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
@ -63,8 +65,11 @@ class _ContactRowState extends State<ContactRow> {
|
|||
child: LinearProgressIndicator(
|
||||
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
|
||||
)),
|
||||
Text(contact.onion,
|
||||
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
|
||||
Visibility(
|
||||
visible: !Provider.of<Settings>(context).streamerMode,
|
||||
child: Text(contact.onion,
|
||||
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor() : Provider.of<Settings>(context).theme.mainTextColor())),
|
||||
)
|
||||
],
|
||||
))),
|
||||
Padding(
|
||||
|
@ -127,9 +132,9 @@ class _ContactRowState extends State<ContactRow> {
|
|||
}
|
||||
// If the last message was over a day ago, just state the date
|
||||
if (DateTime.now().difference(date).inDays > 1) {
|
||||
return DateFormat.yMd().format(date.toLocal());
|
||||
return DateFormat.yMd(Platform.localeName).format(date.toLocal());
|
||||
}
|
||||
// Otherwise just state the time.
|
||||
return DateFormat.Hm().format(date.toLocal());
|
||||
return DateFormat.Hm(Platform.localeName).format(date.toLocal());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/models/message.dart';
|
||||
import 'package:file_picker_desktop/file_picker_desktop.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../main.dart';
|
||||
import '../model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
import 'messagebubbledecorations.dart';
|
||||
|
||||
// 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
|
||||
class FileBubble extends StatefulWidget {
|
||||
final String nameSuggestion;
|
||||
final String rootHash;
|
||||
final String nonce;
|
||||
final int fileSize;
|
||||
final bool interactive;
|
||||
|
||||
FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.interactive = true});
|
||||
|
||||
@override
|
||||
FileBubbleState createState() => FileBubbleState();
|
||||
|
||||
String fileKey() {
|
||||
return this.rootHash + "." + this.nonce;
|
||||
}
|
||||
}
|
||||
|
||||
class FileBubbleState extends State<FileBubble> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
var flagStarted = Provider.of<MessageMetadata>(context).flags & 0x02 > 0;
|
||||
var borderRadiousEh = 15.0;
|
||||
var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
|
||||
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
|
||||
|
||||
// If the sender is not us, then we want to give them a nickname...
|
||||
var senderDisplayStr = "";
|
||||
if (!fromMe) {
|
||||
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
|
||||
if (contact != null) {
|
||||
senderDisplayStr = contact.nickname;
|
||||
} else {
|
||||
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
|
||||
}
|
||||
}
|
||||
|
||||
var wdgSender = Center(
|
||||
widthFactor: 1,
|
||||
child: SelectableText(senderDisplayStr + '\u202F',
|
||||
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())));
|
||||
|
||||
var wdgMessage = !showFileSharing
|
||||
? Text(AppLocalizations.of(context)!.messageEnableFileSharing)
|
||||
: fromMe
|
||||
? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash, widget.fileSize)
|
||||
: (fileChrome(AppLocalizations.of(context)!.messageFileOffered + ":", widget.nameSuggestion, widget.rootHash, widget.fileSize,
|
||||
Provider.of<ProfileInfoState>(context).downloadSpeed(widget.fileKey())));
|
||||
Widget wdgDecorations;
|
||||
if (!showFileSharing) {
|
||||
wdgDecorations = Text('\u202F');
|
||||
} else if (fromMe) {
|
||||
wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
|
||||
} else if (Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey())) {
|
||||
// in this case, whatever marked download.complete would have also set the path
|
||||
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey())!;
|
||||
wdgDecorations = Text(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F');
|
||||
} else if (Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey())) {
|
||||
if (!Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey())) {
|
||||
wdgDecorations = Text(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F');
|
||||
} else {
|
||||
wdgDecorations = LinearProgressIndicator(
|
||||
value: Provider.of<ProfileInfoState>(context).downloadProgress(widget.fileKey()),
|
||||
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor(),
|
||||
);
|
||||
}
|
||||
} else if (flagStarted) {
|
||||
// in this case, the download was done in a previous application launch,
|
||||
// so we probably have to request an info lookup
|
||||
if (!Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey()) ) {
|
||||
wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F');
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.CheckDownloadStatus(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.fileKey());
|
||||
} else {
|
||||
var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
|
||||
wdgDecorations = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children:[Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F'),ElevatedButton(onPressed: _btnResume, child: Text(AppLocalizations.of(context)!.verfiyResumeButton))]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
wdgDecorations = Center(
|
||||
widthFactor: 1,
|
||||
child: Wrap(children: [
|
||||
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F'), onPressed: _btnAccept)),
|
||||
]));
|
||||
}
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
//print(constraints.toString()+", "+constraints.maxWidth.toString());
|
||||
return Center(
|
||||
widthFactor: 1.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(),
|
||||
border:
|
||||
Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor() : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor(), width: 1),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(borderRadiousEh),
|
||||
topRight: Radius.circular(borderRadiousEh),
|
||||
bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
|
||||
bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
widthFactor: 1.0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(9.0),
|
||||
child: Wrap(alignment: WrapAlignment.start, children: [
|
||||
Center(
|
||||
widthFactor: 1.0,
|
||||
child: Column(
|
||||
crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: fromMe
|
||||
? [wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)]
|
||||
: [wdgSender, wdgMessage, Visibility(visible: widget.interactive, child: wdgDecorations)]),
|
||||
)
|
||||
])))));
|
||||
});
|
||||
}
|
||||
|
||||
void _btnAccept() async {
|
||||
String? selectedFileName;
|
||||
File? file;
|
||||
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
|
||||
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
|
||||
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, handle, widget.nameSuggestion, widget.fileKey());
|
||||
} else {
|
||||
try {
|
||||
selectedFileName = await saveFile(
|
||||
defaultFileName: widget.nameSuggestion,
|
||||
);
|
||||
if (selectedFileName != null) {
|
||||
file = File(selectedFileName);
|
||||
print("saving to " + file.path);
|
||||
var manifestPath = file.path + ".manifest";
|
||||
Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x02);
|
||||
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x02;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, handle, file.path, manifestPath, widget.fileKey());
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _btnResume() async {
|
||||
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
var handle = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
|
||||
Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
|
||||
}
|
||||
|
||||
// Construct an file chrome for the sender
|
||||
Widget senderFileChrome(String chrome, String fileName, String rootHash, int fileSize) {
|
||||
return ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [
|
||||
SelectableText(
|
||||
chrome + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
),
|
||||
SelectableText(
|
||||
fileName + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
maxLines: 2,
|
||||
),
|
||||
SelectableText(
|
||||
prettyBytes(fileSize) + '\u202F' + '\n',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
)
|
||||
]),
|
||||
subtitle: SelectableText(
|
||||
'sha512: ' + rootHash + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
fontSize: 10,
|
||||
fontFamily: "monospace",
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 4,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
),
|
||||
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromMeTextColor()));
|
||||
}
|
||||
|
||||
// Construct an file chrome
|
||||
Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) {
|
||||
return ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Wrap(direction: Axis.horizontal, alignment: WrapAlignment.start, children: [
|
||||
SelectableText(
|
||||
chrome + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
),
|
||||
SelectableText(
|
||||
fileName + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
maxLines: 2,
|
||||
),
|
||||
SelectableText(
|
||||
AppLocalizations.of(context)!.labelFilesize + ': ' + prettyBytes(fileSize) + '\u202F' + '\n',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 2,
|
||||
)
|
||||
]),
|
||||
subtitle: SelectableText(
|
||||
'sha512: ' + rootHash + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
fontSize: 10,
|
||||
fontFamily: "monospace",
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 4,
|
||||
textWidthBasis: TextWidthBasis.parent,
|
||||
),
|
||||
leading: Icon(Icons.attach_file, size: 32, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor()),
|
||||
trailing: Visibility(
|
||||
visible: speed != "0 B/s",
|
||||
child: SelectableText(
|
||||
speed + '\u202F',
|
||||
style: TextStyle(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
maxLines: 1,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/cwtch_icons_icons.dart';
|
||||
import 'package:cwtch/models/message.dart';
|
||||
|
@ -39,7 +40,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
|
|||
var borderRadiousEh = 15.0;
|
||||
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
|
||||
rejected = Provider.of<MessageMetadata>(context).flags & 0x01 == 0x01;
|
||||
var prettyDate = DateFormat.yMd().add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
|
||||
var prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(Provider.of<MessageMetadata>(context).timestamp);
|
||||
|
||||
// If the sender is not us, then we want to give them a nickname...
|
||||
var senderDisplayStr = "";
|
||||
|
@ -84,8 +85,8 @@ class InvitationBubbleState extends State<InvitationBubble> {
|
|||
wdgDecorations = Center(
|
||||
widthFactor: 1,
|
||||
child: Wrap(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(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)),
|
||||
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F'), onPressed: _btnReject)),
|
||||
Padding(padding: EdgeInsets.all(5), child: ElevatedButton(child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F'), onPressed: _btnAccept)),
|
||||
]));
|
||||
}
|
||||
|
||||
|
@ -130,7 +131,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
|
|||
var contact = Provider.of<ContactInfoState>(context, listen: false).onion;
|
||||
var idx = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
Provider.of<FlwtchState>(context, listen: false).cwtch.UpdateMessageFlags(profileOnion, contact, idx, Provider.of<MessageMetadata>(context, listen: false).flags | 0x01);
|
||||
Provider.of<MessageMetadata>(context).flags |= 0x01;
|
||||
Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cwtch/models/message.dart';
|
||||
import 'package:cwtch/widgets/malformedbubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
import 'messagebubbledecorations.dart';
|
||||
|
@ -26,9 +32,10 @@ class MessageBubbleState extends State<MessageBubble> {
|
|||
var prettyDate = "";
|
||||
var borderRadiousEh = 15.0;
|
||||
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
|
||||
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
|
||||
|
||||
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
|
||||
prettyDate = DateFormat.yMd().add_jm().format(messageDate.toLocal());
|
||||
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
|
||||
|
||||
// If the sender is not us, then we want to give them a nickname...
|
||||
var senderDisplayStr = "";
|
||||
|
@ -43,16 +50,40 @@ class MessageBubbleState extends State<MessageBubble> {
|
|||
var wdgSender = SelectableText(senderDisplayStr,
|
||||
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
|
||||
|
||||
var wdgMessage = SelectableText(
|
||||
widget.content + '\u202F',
|
||||
//key: Key(myKey),
|
||||
focusNode: _focus,
|
||||
style: TextStyle(
|
||||
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
);
|
||||
var wdgMessage;
|
||||
|
||||
if (!showClickableLinks) {
|
||||
wdgMessage = SelectableText(
|
||||
widget.content + '\u202F',
|
||||
//key: Key(myKey),
|
||||
focusNode: _focus,
|
||||
style: TextStyle(
|
||||
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
);
|
||||
} else {
|
||||
wdgMessage = SelectableLinkify(
|
||||
text: widget.content + '\u202F',
|
||||
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
|
||||
options: LinkifyOptions(humanize: false),
|
||||
linkifiers: [UrlLinkifier()],
|
||||
onOpen: (link) {
|
||||
_modalOpenLink(context, link);
|
||||
},
|
||||
//key: Key(myKey),
|
||||
focusNode: _focus,
|
||||
style: TextStyle(
|
||||
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Provider.of<Settings>(context).current().mainTextColor(),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
);
|
||||
}
|
||||
|
||||
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
|
||||
|
||||
|
@ -88,4 +119,57 @@ class MessageBubbleState extends State<MessageBubble> {
|
|||
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
|
||||
});
|
||||
}
|
||||
|
||||
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
|
||||
showModalBottomSheet<void>(
|
||||
context: ctx,
|
||||
builder: (BuildContext bcontext) {
|
||||
return Container(
|
||||
height: 200, // bespoke value courtesy of the [TextField] docs
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(30.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"Opening this link will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open links from people you trust. Are you sure you want to continue?"
|
||||
),
|
||||
Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
|
||||
child: ElevatedButton(
|
||||
child: Text("Copy link", semanticsLabel: "Copy link"),
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: link.url));
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.copiedClipboardNotification),
|
||||
);
|
||||
|
||||
Navigator.pop(bcontext);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
|
||||
child: ElevatedButton(
|
||||
child: Text("Open link", semanticsLabel: "Open link"),
|
||||
onPressed: () async {
|
||||
if (await canLaunch(link.url)) {
|
||||
await launch(link.url);
|
||||
} else {
|
||||
throw 'Could not launch $link';
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,9 @@ import '../model.dart';
|
|||
import '../settings.dart';
|
||||
|
||||
class MessageList extends StatefulWidget {
|
||||
int initialIndex;
|
||||
ItemScrollController scrollController;
|
||||
ItemPositionsListener scrollListener;
|
||||
MessageList(this.initialIndex, this.scrollController, this.scrollListener);
|
||||
MessageList(this.scrollController, this.scrollListener);
|
||||
|
||||
@override
|
||||
_MessageListState createState() => _MessageListState();
|
||||
|
@ -21,6 +20,7 @@ class MessageList extends StatefulWidget {
|
|||
class _MessageListState extends State<MessageList> {
|
||||
@override
|
||||
Widget build(BuildContext outerContext) {
|
||||
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
|
||||
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
|
||||
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
|
||||
bool isGroupAndSynced = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status == "Synced";
|
||||
|
@ -56,45 +56,44 @@ class _MessageListState extends State<MessageList> {
|
|||
Text("")),
|
||||
))),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
child: Container(
|
||||
// Only show broken heart is the contact is offline...
|
||||
decoration: BoxDecoration(
|
||||
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: loadMessages
|
||||
? ScrollablePositionedList.builder(
|
||||
itemPositionsListener: widget.scrollListener,
|
||||
itemScrollController: widget.scrollController,
|
||||
initialScrollIndex: widget.initialIndex,
|
||||
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 profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
|
||||
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
|
||||
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
|
||||
child: Container(
|
||||
// Only show broken heart is the contact is offline...
|
||||
decoration: BoxDecoration(
|
||||
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: loadMessages
|
||||
? ScrollablePositionedList.builder(
|
||||
itemPositionsListener: widget.scrollListener,
|
||||
itemScrollController: widget.scrollController,
|
||||
initialScrollIndex: initi > 4 ? initi - 4 : 0,
|
||||
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 profileOnion = Provider.of<ProfileInfoState>(outerContext, listen: false).onion;
|
||||
var contactHandle = Provider.of<ContactInfoState>(outerContext, listen: false).onion;
|
||||
var messageIndex = Provider.of<ContactInfoState>(outerContext).totalMessages - index - 1;
|
||||
|
||||
return FutureBuilder(
|
||||
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var message = snapshot.data as Message;
|
||||
// Already includes MessageRow,,
|
||||
return message.getWidget(context);
|
||||
} else {
|
||||
return MessageLoadingBubble();
|
||||
}
|
||||
},
|
||||
);
|
||||
return FutureBuilder(
|
||||
future: messageHandler(outerContext, profileOnion, contactHandle, messageIndex),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var message = snapshot.data as Message;
|
||||
// Already includes MessageRow,,
|
||||
return message.getWidget(context);
|
||||
} else {
|
||||
return Text(''); //MessageLoadingBubble();
|
||||
}
|
||||
},
|
||||
)
|
||||
: null)))
|
||||
);
|
||||
},
|
||||
)
|
||||
: null))
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:cwtch/models/message.dart';
|
|||
import 'package:cwtch/views/contactsview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cwtch/widgets/profileimage.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
|
@ -20,9 +21,30 @@ class MessageRow extends StatefulWidget {
|
|||
MessageRowState createState() => MessageRowState();
|
||||
}
|
||||
|
||||
class MessageRowState extends State<MessageRow> {
|
||||
bool showMenu = false;
|
||||
class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMixin {
|
||||
bool showBlockedMessage = false;
|
||||
late AnimationController _controller;
|
||||
late Animation<Alignment> _animation;
|
||||
late Alignment _dragAlignment = Alignment.center;
|
||||
Alignment _dragAffinity = Alignment.center;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
_controller.addListener(() {
|
||||
setState(() {
|
||||
_dragAlignment = _animation.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
|
||||
|
@ -30,6 +52,12 @@ class MessageRowState extends State<MessageRow> {
|
|||
var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
|
||||
var actualMessage = Flexible(flex: 3, fit: FlexFit.loose, child: widget.child);
|
||||
|
||||
_dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
|
||||
|
||||
if (_dragAlignment == Alignment.center) {
|
||||
_dragAlignment = fromMe ? Alignment.centerRight : Alignment.centerLeft;
|
||||
}
|
||||
|
||||
var senderDisplayStr = "";
|
||||
if (!fromMe) {
|
||||
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
|
||||
|
@ -41,7 +69,7 @@ class MessageRowState extends State<MessageRow> {
|
|||
}
|
||||
|
||||
Widget wdgIcons = Visibility(
|
||||
visible: this.showMenu,
|
||||
visible: Provider.of<AppState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageIndex,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
|
@ -52,7 +80,7 @@ class MessageRowState extends State<MessageRow> {
|
|||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
},
|
||||
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor())));
|
||||
Widget wdgSpacer = Expanded(child: SizedBox(width: 60, height: 10));
|
||||
Widget wdgSpacer = Flexible(child: SizedBox(width: 60, height: 10));
|
||||
var widgetRow = <Widget>[];
|
||||
|
||||
if (fromMe) {
|
||||
|
@ -94,7 +122,6 @@ class MessageRowState extends State<MessageRow> {
|
|||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(blockedMessageBackground),
|
||||
overlayColor: MaterialStateProperty.all(blockedMessageBackground),
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.showMessageButton + '\u202F',
|
||||
|
@ -131,29 +158,94 @@ class MessageRowState extends State<MessageRow> {
|
|||
wdgSpacer,
|
||||
];
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
var size = MediaQuery.of(context).size;
|
||||
var mr = MouseRegion(
|
||||
// For desktop...
|
||||
|
||||
onHover: (event) {
|
||||
setState(() {
|
||||
this.showMenu = true;
|
||||
Provider.of<AppState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
});
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
this.showMenu = false;
|
||||
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onPanUpdate: (details) {
|
||||
setState(() {
|
||||
_dragAlignment += Alignment(
|
||||
details.delta.dx / (size.width * 0.5),
|
||||
0,
|
||||
);
|
||||
});
|
||||
},
|
||||
onPanDown: (details) {
|
||||
_controller.stop();
|
||||
},
|
||||
onPanEnd: (details) {
|
||||
_runAnimation(details.velocity.pixelsPerSecond, size);
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: Align(
|
||||
widthFactor: 1,
|
||||
alignment: _dragAlignment,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: widgetRow,
|
||||
)))));
|
||||
var mark = Provider.of<ContactInfoState>(context).newMarker;
|
||||
if (mark > 0 && mark == Provider.of<ContactInfoState>(context).totalMessages - Provider.of<MessageMetadata>(context).messageIndex) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Align(alignment:Alignment.center ,child:_bubbleNew()), mr]);
|
||||
} else {
|
||||
return mr;
|
||||
}
|
||||
}
|
||||
|
||||
// Swipe to quote on Android
|
||||
onHorizontalDragEnd: Platform.isAndroid
|
||||
? (details) {
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageIndex;
|
||||
}
|
||||
: null,
|
||||
child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow))));
|
||||
Widget _bubbleNew() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
|
||||
border: Border.all(
|
||||
color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor(),
|
||||
width: 1),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(8),
|
||||
bottomRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(9.0),
|
||||
child: Text(AppLocalizations.of(context)!.newMessagesLabel)));
|
||||
}
|
||||
|
||||
void _runAnimation(Offset pixelsPerSecond, Size size) {
|
||||
_animation = _controller.drive(
|
||||
AlignmentTween(
|
||||
begin: _dragAlignment,
|
||||
end: _dragAffinity,
|
||||
),
|
||||
);
|
||||
// Calculate the velocity relative to the unit interval, [0,1],
|
||||
// used by the animation controller.
|
||||
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
|
||||
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
|
||||
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
|
||||
final unitVelocity = unitsPerSecond.distance;
|
||||
|
||||
const spring = SpringDescription(
|
||||
mass: 30,
|
||||
stiffness: 1,
|
||||
damping: 1,
|
||||
);
|
||||
|
||||
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
void _btnGoto() {
|
||||
|
@ -173,14 +265,14 @@ class MessageRowState extends State<MessageRow> {
|
|||
|
||||
showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
|
||||
// set up the buttons
|
||||
Widget cancelButton = TextButton(
|
||||
Widget cancelButton = ElevatedButton(
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // dismiss dialog
|
||||
},
|
||||
);
|
||||
Widget continueButton = TextButton(
|
||||
Widget continueButton = ElevatedButton(
|
||||
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
|
||||
child: Text(AppLocalizations.of(context)!.addContact),
|
||||
onPressed: () {
|
||||
|
|
|
@ -37,6 +37,7 @@ class _CwtchTextFieldState extends State<CwtchPasswordField> {
|
|||
controller: widget.controller,
|
||||
validator: widget.validator,
|
||||
obscureText: obscureText,
|
||||
enableIMEPersonalizedLearning: false,
|
||||
autofillHints: widget.autoFillHints,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
onFieldSubmitted: widget.action,
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:cwtch/widgets/profileimage.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../errorHandler.dart';
|
||||
import '../main.dart';
|
||||
import '../model.dart';
|
||||
import '../settings.dart';
|
||||
|
@ -46,12 +47,14 @@ class _ProfileRowState extends State<ProfileRow> {
|
|||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
profile.onion,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
))
|
||||
Visibility(
|
||||
visible: !Provider.of<Settings>(context).streamerMode,
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
profile.onion,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)))
|
||||
],
|
||||
)),
|
||||
IconButton(
|
||||
|
@ -59,7 +62,7 @@ class _ProfileRowState extends State<ProfileRow> {
|
|||
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
|
||||
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
|
||||
onPressed: () {
|
||||
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
|
||||
_pushEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
@ -98,7 +101,8 @@ class _ProfileRowState extends State<ProfileRow> {
|
|||
);
|
||||
}
|
||||
|
||||
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
|
||||
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
|
||||
Provider.of<ErrorHandler>(context).reset();
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:cwtch/main.dart';
|
||||
import 'package:cwtch/models/servers.dart';
|
||||
import 'package:cwtch/views/addeditservers.dart';
|
||||
import 'package:cwtch/widgets/profileimage.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../cwtch_icons_icons.dart';
|
||||
import '../errorHandler.dart';
|
||||
import '../model.dart';
|
||||
import '../settings.dart';
|
||||
|
||||
class ServerRow extends StatefulWidget {
|
||||
@override
|
||||
_ServerRowState createState() => _ServerRowState();
|
||||
}
|
||||
|
||||
class _ServerRowState extends State<ServerRow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var server = Provider.of<ServerInfoState>(context);
|
||||
return Card(clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.all(0.0),
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6.0), //border size
|
||||
child: Icon(CwtchIcons.dns_24px,
|
||||
color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
|
||||
size: 64)
|
||||
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
server.description,
|
||||
semanticsLabel: server.description,
|
||||
style: Provider.of<FlwtchState>(context).biggerFont.apply(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Visibility(
|
||||
visible: !Provider.of<Settings>(context).streamerMode,
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
server.onion,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: server.running ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
|
||||
)))
|
||||
],
|
||||
)),
|
||||
|
||||
// Copy server button
|
||||
IconButton(
|
||||
enableFeedback: true,
|
||||
tooltip: AppLocalizations.of(context)!.copyServerKeys,
|
||||
icon: Icon(CwtchIcons.address_copy_2, color: Provider.of<Settings>(context).current().mainTextColor()),
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: server.serverBundle));
|
||||
},
|
||||
),
|
||||
|
||||
// Edit button
|
||||
IconButton(
|
||||
enableFeedback: true,
|
||||
tooltip: AppLocalizations.of(context)!.editServerTitle,
|
||||
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
|
||||
onPressed: () {
|
||||
_pushEditServer(server);
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
])));
|
||||
}
|
||||
|
||||
void _pushEditServer(ServerInfoState server ) {
|
||||
Provider.of<ErrorHandler>(context).reset();
|
||||
Navigator.of(context).push(MaterialPageRoute<void>(
|
||||
settings: RouteSettings(name: "serveraddedit"),
|
||||
builder: (BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider<ServerInfoState>(
|
||||
create: (_) => server,
|
||||
)],
|
||||
child: AddEditServerView(),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ class _CwtchTextFieldState extends State<CwtchTextField> {
|
|||
validator: widget.validator,
|
||||
onChanged: widget.onChanged,
|
||||
autofocus: widget.autofocus,
|
||||
enableIMEPersonalizedLearning: false,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
env LD_LIBRARY_PATH=./lib/ ./lib/cwtch
|
||||
exec env LD_LIBRARY_PATH=./lib/ ./lib/cwtch
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/lib/cwtch/cwtch
|
||||
exec env LD_LIBRARY_PATH=~/.local/lib/cwtch/ ~/.local/lib/cwtch/cwtch
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/lib/cwtch/cwtch
|
||||
exec env LD_LIBRARY_PATH=/usr/lib/cwtch /usr/lib/cwtch/cwtch
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mkdir -p ~/.local/bin
|
||||
sed "s|~|$HOME|g" cwtch.home.sh > ~/.local/bin/cwtch
|
||||
chmod a+x ~/.local/bin/cwtch
|
||||
|
||||
mkdir -p ~/.local/share/icons
|
||||
cp cwtch.png ~/.local/share/icons
|
||||
|
@ -13,4 +14,5 @@ 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
|
||||
sed "s|~|$HOME|g" cwtch.home.desktop > $HOME/.local/share/applications/cwtch.desktop
|
||||
chmod a+x $HOME/.local/share/applications/cwtch.desktop
|
|
@ -1,6 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
cp cwtch.sys.sh /usr/bin/cwtch
|
||||
chmod a+x /usr/bin/cwtch
|
||||
|
||||
cp cwtch.png /usr/share/icons
|
||||
|
||||
|
@ -11,3 +12,4 @@ mkdir -p /usr/lib/cwtch
|
|||
cp -r lib/* /usr/lib/cwtch
|
||||
|
||||
cp cwtch.sys.desktop /usr/share/applications/cwtch.desktop
|
||||
chmod a+x /usr/share/applications/cwtch.desktop
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/xcuserdata/
|
|
@ -0,0 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
|
@ -0,0 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
platform :osx, '10.11'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- package_info_plus_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- path_provider_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
package_info_plus_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
|
||||
path_provider_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
||||
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
|
||||
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
|
||||
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
||||
COCOAPODS: 1.11.2
|
|
@ -0,0 +1,634 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||
buildPhases = (
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flutter Assemble";
|
||||
productName = FLX;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
211091843422DC99794C0E66 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||
remoteInfo = FLX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Bundle Framework";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1AF219FB7E04D0D2DBC075A5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
2FFAA895D8F20891DA4D87C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* Cwtch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cwtch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
5EEB7EA2235BC5CDA2BCB6A9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
211091843422DC99794C0E66 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
35B90E5140F9C2DE6D3BD07E /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* Cwtch.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
35B90E5140F9C2DE6D3BD07E /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2FFAA895D8F20891DA4D87C5 /* Pods-Runner.debug.xcconfig */,
|
||||
1AF219FB7E04D0D2DBC075A5 /* Pods-Runner.release.xcconfig */,
|
||||
5EEB7EA2235BC5CDA2BCB6A9 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C147BFC49BDAD7E14E179AF3 /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
F13BA066A536BB902BFE0B8C /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
73F636226F48A847E9232926 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* Cwtch.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0930;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
33CC111A2044C6BA0003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
33CC10EC2044A3C60003C045 /* Runner */,
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n\n";
|
||||
};
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||
);
|
||||
inputPaths = (
|
||||
Flutter/ephemeral/tripwire,
|
||||
);
|
||||
outputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
73F636226F48A847E9232926 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F13BA066A536BB902BFE0B8C /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
33CC10F52044A3C60003C045 /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_NAME = Cwtch;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_NAME = Cwtch;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_NAME = Cwtch;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10F92044A3C60003C045 /* Debug */,
|
||||
33CC10FA2044A3C60003C045 /* Release */,
|
||||
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10FC2044A3C60003C045 /* Debug */,
|
||||
33CC10FD2044A3C60003C045 /* Release */,
|
||||
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC111C2044C6BA0003C045 /* Debug */,
|
||||
33CC111D2044C6BA0003C045 /* Release */,
|
||||
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1000"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Cwtch.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Cwtch.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Cwtch.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "Cwtch.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,9 @@
|
|||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 207 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 6.4 KiB |
|
@ -0,0 +1,339 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
|
@ -0,0 +1,14 @@
|
|||
// Application-level settings for the Runner target.
|
||||
//
|
||||
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||
// future. If not, the values below would default to using the project name when this becomes a
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = ui
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = im.cwtch.ui
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2021 Open Privacy Research Society. All rights reserved.
|
|
@ -0,0 +1,2 @@
|
|||
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
|
@ -0,0 +1,2 @@
|
|||
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
|
@ -0,0 +1,13 @@
|
|||
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||
CLANG_WARN_PRAGMA_PACK = YES
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||
CLANG_WARN_COMMA = YES
|
||||
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||
GCC_WARN_SHADOW = YES
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,15 @@
|
|||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController.init()
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
self.setFrame(windowFrame, display: true)
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,25 @@
|
|||
input_filepath="../cwtch.png"
|
||||
output_iconset_name="cwtch.iconset"
|
||||
mkdir $output_iconset_name
|
||||
|
||||
sips -z 16 16 $input_filepath --out "${output_iconset_name}/icon_16x16.png"
|
||||
cp "${output_iconset_name}/icon_16x16.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
|
||||
sips -z 32 32 $input_filepath --out "${output_iconset_name}/icon_16x16@2x.png"
|
||||
sips -z 32 32 $input_filepath --out "${output_iconset_name}/icon_32x32.png"
|
||||
cp "${output_iconset_name}/icon_32x32.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
|
||||
sips -z 64 64 $input_filepath --out "${output_iconset_name}/icon_32x32@2x.png"
|
||||
cp "${output_iconset_name}/icon_32x32@2x.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
|
||||
sips -z 128 128 $input_filepath --out "${output_iconset_name}/icon_128x128.png"
|
||||
cp "${output_iconset_name}/icon_128x128.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
|
||||
sips -z 256 256 $input_filepath --out "${output_iconset_name}/icon_128x128@2x.png"
|
||||
sips -z 256 256 $input_filepath --out "${output_iconset_name}/icon_256x256.png"
|
||||
cp "${output_iconset_name}/icon_256x256.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
|
||||
sips -z 512 512 $input_filepath --out "${output_iconset_name}/icon_256x256@2x.png"
|
||||
sips -z 512 512 $input_filepath --out "${output_iconset_name}/icon_512x512.png"
|
||||
cp "${output_iconset_name}/icon_512x512.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
|
||||
sips -z 1024 1024 $input_filepath --out "${output_iconset_name}/icon_1024x1024.png"
|
||||
cp "${output_iconset_name}/icon_1024x1024.png" Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
|
||||
|
||||
iconutil -c icns $output_iconset_name
|
||||
|
||||
rm -R $output_iconset_name
|