Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02b4a73de7 | |||
| 0f0a647c3c | |||
| 86de81adb3 | |||
| 7d32be57a9 | |||
| bc1c8589c3 | |||
| 6b49e1d5e4 | |||
| 7790eea069 | |||
| dd9d990e3f | |||
| 504e0af11a | |||
| 7248e6096d | |||
| c183c819fc | |||
| 28122fa003 | |||
| e2e9f78705 | |||
| 49444b3056 | |||
| 4e8ac3fafb | |||
| ff6cd50cc5 | |||
| 67df1c2b7b | |||
| c464724bc8 | |||
| 6af87aa69c | |||
| fad1873369 | |||
| ee533bd76a | |||
| 6b5d4c3958 | |||
| 4c81a280c8 | |||
| 368f9ef617 | |||
| deeda09ca3 | |||
| 28a70074c8 | |||
| 0b7c618f33 | |||
| 0118d37232 | |||
| 4f5ba12f2d | |||
| 2275b582b4 | |||
| f635df038a | |||
| 36c3acdf2c | |||
| 6704fc7cbb | |||
| 34f345c237 | |||
| 127b798acd | |||
| c2e39982a1 | |||
| e1d5e29365 | |||
| 70dbf3966d | |||
| 493ea01f85 | |||
| cf0cc8b305 | |||
| 46e292193c | |||
| 19d429d739 | |||
| 451294ba08 | |||
| f3182675de | |||
| 80a0a4ef63 | |||
| 23117fd96f | |||
| d6b6fde480 | |||
| 44892e5b03 | |||
| 268e794f5d | |||
| 7f88684d8c | |||
| 17b4b62133 | |||
| 4d3b33d877 | |||
| 1916d4f922 | |||
| 3ea1f05460 | |||
| 1eaa306a70 | |||
| 9fa683e7e4 | |||
| 2292013507 | |||
| 13b60b92bf |
@@ -4,7 +4,7 @@ include: package:flutter_lints/flutter.yaml
|
||||
linter:
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
rules:
|
||||
prefer_double_quotes: true
|
||||
prefer_single_quotes: true
|
||||
always_declare_return_types: true
|
||||
avoid_dynamic_calls: true
|
||||
avoid_empty_else: true
|
||||
@@ -42,4 +42,4 @@ linter:
|
||||
unrelated_type_equality_checks: true
|
||||
unsafe_html: true
|
||||
use_full_hex_values_for_flutter_colors: true
|
||||
valid_regexps: true
|
||||
valid_regexps: true
|
||||
|
||||
@@ -43,8 +43,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "chat.simplex.app"
|
||||
minSdkVersion 16
|
||||
applicationId "com.example.simplex_chat"
|
||||
minSdkVersion 20
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="chat.simplex.app">
|
||||
package="com.example.simplex_chat">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="chat.simplex.app">
|
||||
package="com.example.simplex_chat">
|
||||
<application
|
||||
android:label="simplex_chat"
|
||||
android:label="SimpleX Chat"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package chat.simplex.app
|
||||
package com.example.simplex_chat
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 10 KiB |
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="chat.simplex.app">
|
||||
package="com.example.simplex_chat">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="815" height="233" viewBox="0 0 815 233" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5683 66.9141C43.9954 66.9141 47.376 67.2323 50.7104 67.8687C54.0448 68.5051 57.2403 69.3234 60.2968 70.3235C63.3533 71.3236 66.2014 72.4601 68.8411 73.7329C71.4809 75.0058 73.9122 76.2786 76.1351 77.5515L66.132 96.0987L65.9952 95.9474C65.6001 95.5596 64.6732 94.9282 63.2144 94.0531C61.5472 93.053 59.4632 92.0074 56.9624 90.9164C54.4616 89.8254 51.7293 88.8708 48.7654 88.0525C45.8015 87.2342 42.8376 86.8251 39.8737 86.8251C31.723 86.8251 27.6476 89.5072 27.6476 94.8713C27.6476 96.5079 28.0876 97.8716 28.9675 98.9627C29.8474 100.054 31.1441 101.031 32.8576 101.895C34.5711 102.758 36.7245 103.554 39.318 104.281C41.9114 105.009 44.9216 105.827 48.3486 106.736C53.0723 108.009 57.3329 109.395 61.1304 110.896C64.9279 112.396 68.1465 114.26 70.7862 116.487C73.4259 118.715 75.4636 121.419 76.8993 124.602C78.3349 127.784 79.0527 131.602 79.0527 136.057C79.0527 141.512 78.0107 146.126 75.9267 149.899C73.8427 153.673 71.0872 156.718 67.6602 159.037C64.2332 161.355 60.2968 163.037 55.8509 164.083C51.4051 165.128 46.8203 165.651 42.0966 165.651C38.4844 165.651 34.7795 165.378 30.982 164.833C27.1845 164.287 23.4796 163.492 19.8674 162.446C16.2551 161.401 12.7587 160.15 9.37796 158.696C5.99726 157.241 2.87128 155.559 0 153.65L10.0032 134.148L10.1812 134.338C10.6877 134.825 11.8324 135.625 13.6154 136.739C15.6531 138.012 18.177 139.285 21.1872 140.558C24.1974 141.83 27.555 142.967 31.2599 143.967C34.9647 144.967 38.7159 145.467 42.5134 145.467C50.5715 145.467 54.6006 143.058 54.6006 138.239C54.6006 136.421 53.9985 134.921 52.7944 133.739C51.5903 132.557 49.9231 131.489 47.7928 130.534C45.6625 129.579 43.1386 128.693 40.221 127.875C37.3034 127.056 34.1311 126.147 30.7041 125.147C26.1657 123.783 22.2292 122.306 18.8948 120.715C15.5605 119.124 12.805 117.283 10.6284 115.192C8.45174 113.1 6.83086 110.691 5.76571 107.964C4.70056 105.236 4.16798 102.054 4.16798 98.4171C4.16798 93.3257 5.14051 88.8253 7.08557 84.9158C9.03063 81.0063 11.6703 77.7106 15.0047 75.0285C18.3391 72.3464 22.2061 70.3235 26.6056 68.9597C31.0051 67.5959 35.6594 66.9141 40.5683 66.9141ZM131.47 67.7323V164.56H108.685V67.7323H131.47ZM191.39 67.7323L216.954 118.328L242.657 67.7323H267.248V164.56H244.463V106.6L223.067 148.74H210.841L189.445 106.6V164.56H166.66V67.7323H191.39ZM419.925 67.7323V144.922H467.718V164.56H397.14V67.7323H419.925ZM560.072 67.7323V87.3706H514.78V106.191H553.681V124.329H514.78V144.922H561.323V164.56H491.995V67.7323H560.072ZM341.818 67.7323L343.378 67.7677C347.5 67.9563 351.31 68.8991 354.809 70.5962C358.745 72.5055 362.126 75.0058 364.951 78.097C367.776 81.1882 369.999 84.6885 371.62 88.598C373.24 92.5075 374.051 96.4624 374.051 100.463C374.051 104.736 373.287 108.827 371.758 112.737C370.23 116.646 368.1 120.147 365.368 123.238C362.635 126.329 359.324 128.784 355.434 130.602C351.976 132.218 348.189 133.116 344.072 133.296L342.513 133.33H322.507V164.56H299.722V67.7323H341.818ZM340.29 87.3706H322.507V113.828H341.124C343.81 113.828 346.125 112.691 348.07 110.418C350.016 108.145 350.988 104.827 350.988 100.463C350.988 98.1898 350.687 96.2351 350.085 94.5986C349.483 92.9621 348.672 91.5983 347.654 90.5073C346.635 89.4163 345.477 88.6207 344.18 88.1207C342.884 87.6206 341.587 87.3706 340.29 87.3706Z" fill="#062D56"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M642.628 136.08L680.309 173.782L699.513 154.567L699.506 154.561L737.917 116.134L700.236 78.4367L700.243 78.4334L681.404 59.5826L642.993 98.014L642.99 98.0104L681.401 59.5829L643.725 21.881L662.929 2.6652L700.605 40.3673L739.016 1.93511L757.855 20.7859L719.443 59.2176L757.121 96.918L795.533 58.4875L814.373 77.3382L775.959 115.768L813.643 153.471L794.439 172.687L756.756 134.984L718.348 173.415L756.031 211.119L736.827 230.335L699.144 192.63L660.74 231.065L641.901 212.214L680.306 173.78L642.625 136.083L642.628 136.08Z" fill="#07B4B9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M604.77 59.7651L642.446 97.4664L680.856 59.035L699.696 77.8858L661.285 116.317L698.966 154.019L679.762 173.235L642.081 135.532L603.675 173.965L584.836 155.114L623.243 116.682L585.566 78.9809L604.77 59.7651Z" fill="#062D56"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="815" height="233" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="18" viewBox="0 0 24 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12H15" stroke="#0D1333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 6H21" stroke="#0D1333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="Rectangle_2" data-name="Rectangle 2" width="2971" height="843" transform="translate(0 0.292)" fill="#fff"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip-simpleX">
|
||||
<rect width="1024" height="1024"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="simpleX" clip-path="url(#clip-simpleX)">
|
||||
<g id="logo" transform="translate(-2047 89.708)" clip-path="url(#clip-path)">
|
||||
<path id="Path_6" data-name="Path 6" d="M147.9,66.914a198.176,198.176,0,0,1,36.975,3.455,284.4,284.4,0,0,1,34.949,8.885,253.446,253.446,0,0,1,31.15,12.34q14.436,6.911,26.592,13.821L241.1,172.542l-.5-.548q-2.161-2.105-10.138-6.856a202.569,202.569,0,0,0-22.793-11.353,224.542,224.542,0,0,0-29.884-10.365,122.159,122.159,0,0,0-32.417-4.442q-44.573,0-44.573,29.122,0,8.885,4.812,14.808t14.182,10.613a133.233,133.233,0,0,0,23.553,8.636q14.182,3.952,32.923,8.885a437.841,437.841,0,0,1,46.6,15.056q20.767,8.143,35.2,20.236A82.161,82.161,0,0,1,280.355,275.7q7.851,17.275,7.851,41.459,0,29.615-11.4,50.1a94.466,94.466,0,0,1-30.137,33.073A129.137,129.137,0,0,1,203.618,418.6a219.939,219.939,0,0,1-50.145,5.675,287.161,287.161,0,0,1-40.521-2.961,299.792,299.792,0,0,1-40.521-8.639A327.515,327.515,0,0,1,34.19,399.1,215.84,215.84,0,0,1,0,380.839l36.469-70.584.649.688q2.77,2.644,12.52,8.69a199.248,199.248,0,0,0,27.6,13.822,287.382,287.382,0,0,0,36.722,12.338,157.851,157.851,0,0,0,41.027,5.429q44.067,0,44.067-26.16a21.773,21.773,0,0,0-6.585-16.287q-6.585-6.417-18.235-11.6a201.006,201.006,0,0,0-27.6-9.624q-15.955-4.446-34.7-9.874a304.855,304.855,0,0,1-43.054-16.041Q50.651,253,38.748,241.647a72.548,72.548,0,0,1-17.728-26.16Q15.2,200.676,15.2,180.933q0-27.641,10.637-48.865A105.947,105.947,0,0,1,54.7,96.283,126.757,126.757,0,0,1,97,74.318,172.123,172.123,0,0,1,147.9,66.914Zm331.4,2.961v350.45H396.237V69.875Zm218.452,0L790.957,253,884.663,69.875h89.652v350.45H891.247V210.55l-78,152.518H768.671l-78-152.518V420.325H607.6V69.875Zm833.178,0V349.249h174.241v71.076H1447.868V69.875Zm510.939,0v71.077H1876.752V209.07h141.823v65.647H1876.752v74.533h169.683v71.076H1793.684V69.875Zm-795.7,0,5.687.128a106.112,106.112,0,0,1,41.674,10.237,121.073,121.073,0,0,1,36.975,27.148,129.172,129.172,0,0,1,24.313,38.006,110.611,110.611,0,0,1,8.863,42.943,120.122,120.122,0,0,1-8.36,44.423,126.219,126.219,0,0,1-23.3,38.006,109.262,109.262,0,0,1-77.64,36.4l-5.684.123h-72.937V420.325h-83.068V69.875Zm-5.571,71.077h-64.832V236.71h67.873q14.689,0,25.323-12.342t10.638-36.03q0-12.341-3.292-21.225a43.245,43.245,0,0,0-8.863-14.808,33.932,33.932,0,0,0-12.665-8.638A39.4,39.4,0,0,0,1240.607,140.953Z" transform="translate(0 175.261)" fill="#062d56" fill-rule="evenodd"/>
|
||||
<path id="Path_7" data-name="Path 7" d="M644.551,487.448,781.926,623.9l70.013-69.545-.026-.022L991.95,415.257,854.575,278.818l.025-.012-68.682-68.227-140.036,139.1-.011-.013L785.908,210.581,648.551,74.126,718.563,4.578,855.92,141.033,995.957,1.935l68.682,68.227L924.6,209.258l137.364,136.45L1202,206.616l68.685,68.227L1130.641,413.932l137.386,136.459-70.013,69.549L1060.632,483.481,920.607,622.575l137.382,136.463-70.013,69.549L850.594,692.12,710.583,831.228,641.9,763,781.916,623.9,644.541,487.458Z" transform="translate(1698.3 5.061)" fill="#07b4b9" fill-rule="evenodd"/>
|
||||
<path id="Path_8" data-name="Path 8" d="M657.51,61.677,794.867,198.13,934.9,59.035l68.686,68.227L863.549,266.357l137.375,136.455-70.013,69.549L793.536,335.9,653.518,475l-68.682-68.228,140.022-139.1L587.5,131.225Z" transform="translate(1547.321 154.623)" fill="#062d56" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 80 KiB |
@@ -1 +0,0 @@
|
||||
{"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
|
||||
@@ -1 +0,0 @@
|
||||
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]}]
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
|
||||
# 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', '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 Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
PODS:
|
||||
- DKImagePickerController/Core (4.3.2):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.2)
|
||||
- DKImagePickerController/PhotoGallery (4.3.2):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.2)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- image_picker (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage (5.12.0):
|
||||
- SDWebImage/Core (= 5.12.0)
|
||||
- SDWebImage/Core (5.12.0)
|
||||
- share (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- image_picker (from `.symlinks/plugins/image_picker/ios`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- share (from `.symlinks/plugins/share/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
image_picker:
|
||||
:path: ".symlinks/plugins/image_picker/ios"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
share:
|
||||
:path: ".symlinks/plugins/share/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
SDWebImage: 4ea20cca2986adc5aacde07aa686742fd4c67a37
|
||||
share: 0b2c3e82132f5888bccca3351c504d0003b3b410
|
||||
SwiftyGif: 5d4af95df24caf1c570dbbcb32a3b8a0763bc6d7
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
||||
COCOAPODS: 1.11.0
|
||||
@@ -13,6 +13,7 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
AE3860269BF424274DA84C73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -29,12 +30,16 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
031FDA72D548A2F92732ADA4 /* 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>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
2FD15BE67425B26EB06BB4AE /* 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>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7E09253D517C54DFA8ECEE7A /* 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>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -49,12 +54,32 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AE3860269BF424274DA84C73 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
415DD71D33C5D1C1BBA9E62B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B0B8C697FBAC21A75BCBB5A /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5A0F33679D6B606E9775E794 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7E09253D517C54DFA8ECEE7A /* Pods-Runner.debug.xcconfig */,
|
||||
2FD15BE67425B26EB06BB4AE /* Pods-Runner.release.xcconfig */,
|
||||
031FDA72D548A2F92732ADA4 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -72,6 +97,8 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
5A0F33679D6B606E9775E794 /* Pods */,
|
||||
415DD71D33C5D1C1BBA9E62B /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -105,12 +132,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
AA6465993557C0297F2D0450 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
B4CC1E87FA75B139B238C706 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -197,6 +226,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
AA6465993557C0297F2D0450 /* [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;
|
||||
};
|
||||
B4CC1E87FA75B139B238C706 /* [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;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -291,7 +359,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -415,7 +483,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -434,7 +502,7 @@
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.simplexChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -468,4 +536,4 @@
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 824 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 8.6 KiB |
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Animator extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration time;
|
||||
|
||||
const Animator(this.child, this.time, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AnimatorState createState() => _AnimatorState();
|
||||
}
|
||||
|
||||
class _AnimatorState extends State<Animator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Timer? timer;
|
||||
AnimationController? animationController;
|
||||
Animation? animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 290), vsync: this);
|
||||
animation =
|
||||
CurvedAnimation(parent: animationController!, curve: Curves.easeInOut);
|
||||
timer = Timer(widget.time, animationController!.forward);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController!.dispose();
|
||||
super.dispose();
|
||||
timer!.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation!,
|
||||
child: widget.child,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return Opacity(
|
||||
opacity: animation!.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0.0, (1 - animation!.value) * 20),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Timer? timer;
|
||||
Duration duration = const Duration();
|
||||
|
||||
Duration wait() {
|
||||
if (timer == null || !timer!.isActive) {
|
||||
timer = Timer(const Duration(microseconds: 120), () {
|
||||
duration = const Duration();
|
||||
});
|
||||
}
|
||||
duration += const Duration(milliseconds: 100);
|
||||
return duration;
|
||||
}
|
||||
|
||||
class WidgetAnimator extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const WidgetAnimator({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Animator(child, wait());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EntranceFader extends StatefulWidget {
|
||||
/// Child to be animated on entrance
|
||||
final Widget child;
|
||||
|
||||
/// Delay after which the animation will start
|
||||
final Duration delay;
|
||||
|
||||
/// Duration of entrance animation
|
||||
final Duration duration;
|
||||
|
||||
/// Starting point from which the widget will fade to its default position
|
||||
final Offset offset;
|
||||
|
||||
const EntranceFader({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.delay = const Duration(milliseconds: 0),
|
||||
this.duration = const Duration(milliseconds: 400),
|
||||
this.offset = const Offset(0.0, 32.0),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
EntranceFaderState createState() {
|
||||
return EntranceFaderState();
|
||||
}
|
||||
}
|
||||
|
||||
class EntranceFaderState extends State<EntranceFader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController? _controller;
|
||||
Animation? _dxAnimation;
|
||||
Animation? _dyAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: widget.duration);
|
||||
_dxAnimation =
|
||||
Tween(begin: widget.offset.dx, end: 0.0).animate(_controller!);
|
||||
_dyAnimation =
|
||||
Tween(begin: widget.offset.dy, end: 0.0).animate(_controller!);
|
||||
Future.delayed(widget.delay, () {
|
||||
if (mounted) {
|
||||
_controller!.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller!.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller!,
|
||||
builder: (context, child) => Opacity(
|
||||
opacity: _controller!.value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(_dxAnimation!.value, _dyAnimation!.value),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/// All the named routes are mentioned here
|
||||
|
||||
class AppRoutes {
|
||||
static const splash = '/splash';
|
||||
static const intro = '/intro';
|
||||
static const setupProfile = '/setupProfile';
|
||||
static const home = '/home';
|
||||
static const addContact = '/addContact';
|
||||
static const addContactWeb = '/addContactWeb';
|
||||
static const scanInvitation = '/ScanInvitation';
|
||||
static const addGroup = '/addGroup';
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// colors
|
||||
const kPrimaryColor = Color(0xff062d56);
|
||||
const kSecondaryColor = Color(0xff07b4b9);
|
||||
|
||||
// text styles
|
||||
const TextStyle kHeadingStyle = TextStyle(
|
||||
fontSize: 28.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.3,
|
||||
color: kPrimaryColor,
|
||||
);
|
||||
|
||||
const TextStyle kMediumHeadingStyle = TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kPrimaryColor,
|
||||
);
|
||||
|
||||
const TextStyle kSmallHeadingStyle = TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScrollBehaviorModified extends ScrollBehavior {
|
||||
const ScrollBehaviorModified();
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.android:
|
||||
return const BouncingScrollPhysics();
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return const ClampingScrollPhysics();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// ignore_for_file: directives_ordering
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:file_picker/_internal/file_picker_web.dart';
|
||||
import 'package:image_picker_for_web/image_picker_for_web.dart';
|
||||
import 'package:shared_preferences_web/shared_preferences_web.dart';
|
||||
|
||||
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
|
||||
// ignore: public_member_api_docs
|
||||
void registerPlugins(Registrar registrar) {
|
||||
FilePickerWeb.registerWith(registrar);
|
||||
ImagePickerPlugin.registerWith(registrar);
|
||||
SharedPreferencesPlugin.registerWith(registrar);
|
||||
registrar.registerMessageHandler();
|
||||
}
|
||||
@@ -1,115 +1,67 @@
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplex_chat/app_routes.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/custom_scroll_behavior.dart';
|
||||
import 'package:simplex_chat/providers/drawer_providers.dart';
|
||||
import 'package:simplex_chat/views/contacts/add_contact_view.dart';
|
||||
import 'package:simplex_chat/views/contacts/add_contact_web.dart';
|
||||
import 'package:simplex_chat/views/group/add_group_view.dart';
|
||||
import 'package:simplex_chat/views/home/home_view.dart';
|
||||
import 'package:simplex_chat/views/onboarding/intro_view.dart';
|
||||
import 'package:simplex_chat/views/scan_invitation/scan_invitation_view.dart';
|
||||
import 'package:simplex_chat/views/setup_profile_view.dart';
|
||||
import 'package:simplex_chat/views/splash_screen.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
/// Basic Structure is setup on [Providers]
|
||||
/// Navigations are [namedRoutes]
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
setPathUrlStrategy();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: "Flutter Demo",
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// Try running your application with "flutter run". You'll see the
|
||||
// application has a blue toolbar. Then, without quitting the app, try
|
||||
// changing the primarySwatch below to Colors.green and then invoke
|
||||
// "hot reload" (press "r" in the console where you ran "flutter run",
|
||||
// or simply save your changes to "hot reload" in a Flutter IDE).
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// is not restarted.
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: const MyHomePage(title: "Flutter Demo Home Page"),
|
||||
final theme = ThemeData(
|
||||
primarySwatch: Colors.teal,
|
||||
primaryColor: kPrimaryColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({Key? key, required this.title}) : super(key: key);
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Invoke "debug painting" (press "p" in the console, choose the
|
||||
// "Toggle Debug Paint" action from the Flutter Inspector in Android
|
||||
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
|
||||
// to see the wireframe for each widget.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"You have pushed the button this many times:",
|
||||
),
|
||||
Text(
|
||||
"$_counter",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
],
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => DrawerProvider()),
|
||||
],
|
||||
child: Consumer<DrawerProvider>(
|
||||
builder: (context, drawerProvider, chidl) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'SimpleX Chat',
|
||||
theme: theme.copyWith(
|
||||
colorScheme: theme.colorScheme.copyWith(secondary: kPrimaryColor),
|
||||
primaryColorLight: Colors.white,
|
||||
),
|
||||
builder: (context, widget) {
|
||||
return ScrollConfiguration(
|
||||
behavior: const ScrollBehaviorModified(), child: widget!);
|
||||
},
|
||||
initialRoute: AppRoutes.splash,
|
||||
routes: <String, WidgetBuilder>{
|
||||
AppRoutes.splash: (_) => const SplashScreen(),
|
||||
AppRoutes.intro: (_) => const IntroView(),
|
||||
AppRoutes.setupProfile: (_) => const SetupProfileView(),
|
||||
AppRoutes.home: (_) => const HomeView(),
|
||||
AppRoutes.addContact: (_) => const AddContactView(),
|
||||
AppRoutes.addContactWeb: (_) => const AddContactWeb(),
|
||||
AppRoutes.scanInvitation: (_) => const ScanInvitationView(),
|
||||
AppRoutes.addGroup: (_) => const AddGroupView(),
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: "Increment",
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Contact {
|
||||
final String? name;
|
||||
final String? subtitle;
|
||||
final String? photo;
|
||||
final bool? isGroup;
|
||||
|
||||
Contact({
|
||||
required this.name,
|
||||
required this.subtitle,
|
||||
this.photo = '',
|
||||
this.isGroup = false,
|
||||
});
|
||||
|
||||
factory Contact.fromJson(Map<String, dynamic> json) {
|
||||
return Contact(
|
||||
name: json['name'],
|
||||
subtitle: json['subtitle'],
|
||||
photo: json['photo'],
|
||||
isGroup: json['isGroup'],
|
||||
);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> toJson(Contact contact) {
|
||||
return {
|
||||
'name': contact.name,
|
||||
'subtitle': contact.subtitle,
|
||||
'photo': contact.photo,
|
||||
'isGroup': false,
|
||||
};
|
||||
}
|
||||
|
||||
static String encode(List<Contact> contacts) => json.encode(
|
||||
contacts
|
||||
.map<Map<String, dynamic>>((contact) => Contact.toJson(contact))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
static List<Contact> decode(String contacts) =>
|
||||
(json.decode(contacts) as List<dynamic>)
|
||||
.map<Contact>((item) => Contact.fromJson(item))
|
||||
.toList();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Group {
|
||||
final String? name;
|
||||
final String? subtitle;
|
||||
final String? photo;
|
||||
final List<dynamic>? members;
|
||||
final bool? isGroup;
|
||||
|
||||
Group({
|
||||
required this.name,
|
||||
required this.subtitle,
|
||||
this.photo = '',
|
||||
this.isGroup = true,
|
||||
this.members = const [],
|
||||
});
|
||||
|
||||
factory Group.fromJson(Map<String, dynamic> json) {
|
||||
return Group(
|
||||
name: json['name'],
|
||||
subtitle: json['subtitle'],
|
||||
photo: json['photo'],
|
||||
isGroup: json['isGroup'],
|
||||
members: json['members'],
|
||||
);
|
||||
}
|
||||
|
||||
static Map<String, dynamic> toJson(Group group) {
|
||||
return {
|
||||
'name': group.name,
|
||||
'subtitle': group.subtitle,
|
||||
'photo': group.photo,
|
||||
'isGroup': true,
|
||||
'members': group.members,
|
||||
};
|
||||
}
|
||||
|
||||
static String encode(List<Group> groups) => json.encode(
|
||||
groups
|
||||
.map<Map<String, dynamic>>((group) => Group.toJson(group))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
static List<Group> decode(String groups) =>
|
||||
(json.decode(groups) as List<dynamic>)
|
||||
.map<Group>((item) => Group.fromJson(item))
|
||||
.toList();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class DrawerProvider extends ChangeNotifier {
|
||||
int _currentIndex = 1;
|
||||
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
set currentIndex(int value) {
|
||||
_currentIndex = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
class AddContactView extends StatefulWidget {
|
||||
const AddContactView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AddContactViewState createState() => _AddContactViewState();
|
||||
}
|
||||
|
||||
class _AddContactViewState extends State<AddContactView> {
|
||||
bool? _dontShow = false;
|
||||
final qrKey = GlobalKey(debugLabel: 'qr');
|
||||
QRViewController? _qrViewController;
|
||||
Barcode? result;
|
||||
|
||||
// alert dialgoue
|
||||
void _initialWarning() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Are you Sure?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Your profile will be sent to your contact!'),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _dontShow,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dontShow = value;
|
||||
});
|
||||
}),
|
||||
const Text("Don't ask again")
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.check, color: Colors.green),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.cancel_outlined, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _intiBox() {
|
||||
Future.delayed(const Duration(seconds: 1), _initialWarning);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_intiBox();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_qrViewController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Add Contact'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: FutureBuilder(
|
||||
future: _qrViewController?.getFlashStatus(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Icon(snapshot.data == false
|
||||
? Icons.flash_on
|
||||
: Icons.flash_off);
|
||||
}
|
||||
return const Icon(Icons.flash_off);
|
||||
},
|
||||
),
|
||||
onPressed: () async {
|
||||
await _qrViewController?.toggleFlash();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: FutureBuilder(
|
||||
future: _qrViewController?.getCameraInfo(),
|
||||
builder: (context, snapshot) {
|
||||
return const Icon(Icons.camera_alt);
|
||||
},
|
||||
),
|
||||
onPressed: () async {
|
||||
await _qrViewController?.flipCamera();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
child: _qrViewBuild()),
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Tap Here!',
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).size.height * 0.15,
|
||||
left: MediaQuery.of(context).size.width * 0.125,
|
||||
child: const Text(
|
||||
'Position QR Code within the frame.',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
QRView _qrViewBuild() {
|
||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400)
|
||||
? 220.0
|
||||
: 370.0;
|
||||
|
||||
return QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: (QRViewController controller) {
|
||||
_qrViewController = controller;
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
setState(() async {
|
||||
result = scanData;
|
||||
await controller.stopCamera();
|
||||
if (result != null) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 0,
|
||||
borderLength: 50,
|
||||
borderWidth: 10,
|
||||
cutOutSize: scanArea,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
class AddContactWeb extends StatefulWidget {
|
||||
const AddContactWeb({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AddContactWebState createState() => _AddContactWebState();
|
||||
}
|
||||
|
||||
class _AddContactWebState extends State<AddContactWeb> {
|
||||
bool? _dontShow = false;
|
||||
|
||||
// alert dialgoue
|
||||
void _initialWarning() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Are you Sure?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Your profile will be sent to your contact!'),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _dontShow,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_dontShow = value;
|
||||
});
|
||||
}),
|
||||
const Text("Don't ask again")
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.check, color: Colors.green),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.cancel_outlined, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _intiBox() {
|
||||
Future.delayed(const Duration(seconds: 1), _initialWarning);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_intiBox();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('Add Contact'),
|
||||
),
|
||||
body: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: Colors.red,
|
||||
width: 5.0,
|
||||
),
|
||||
),
|
||||
child: Image.asset('assets/code.png')),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).size.height * 0.15,
|
||||
left: MediaQuery.of(context).size.width * 0.41,
|
||||
child: const Text(
|
||||
'Position QR Code within the frame.',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/animations/bottom_animation.dart';
|
||||
import 'package:simplex_chat/app_routes.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/model/contact.dart';
|
||||
import 'package:simplex_chat/model/group.dart';
|
||||
import 'package:simplex_chat/views/conversation/conversation_view.dart';
|
||||
|
||||
class Conversations extends StatefulWidget {
|
||||
const Conversations({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ConversationsState createState() => _ConversationsState();
|
||||
}
|
||||
|
||||
class _ConversationsState extends State<Conversations> {
|
||||
bool? _eraseMedia = false;
|
||||
|
||||
List<dynamic> _conversations = [];
|
||||
|
||||
List<Contact> _contactsList = []; // for storing contacts
|
||||
List<Group> _groupList = []; // for storing groups
|
||||
|
||||
final List<String> _options = [
|
||||
'Add contact',
|
||||
'Scan invitation',
|
||||
'Add group',
|
||||
];
|
||||
|
||||
// getting data from local storage FOR NOW!!
|
||||
void _getContacts() async {
|
||||
debugPrint('Getting contacts!');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? _contacts = prefs.getString('contacts');
|
||||
if (_contacts != null) {
|
||||
setState(() {
|
||||
_contactsList = List.from(Contact.decode(_contacts));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// getting data from local storage FOR NOW!!
|
||||
void _getGroups() async {
|
||||
debugPrint('Getting groups!');
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? _groups = prefs.getString('groups');
|
||||
if (_groups != null) {
|
||||
setState(() {
|
||||
_groupList = List.from(Group.decode(_groups));
|
||||
});
|
||||
}
|
||||
|
||||
_gettingGroupContactsChats();
|
||||
}
|
||||
|
||||
void _gettingGroupContactsChats() {
|
||||
debugPrint('Merging!');
|
||||
setState(() {
|
||||
_conversations = List.from(_contactsList);
|
||||
_conversations = _conversations + _groupList;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getContacts();
|
||||
_getGroups();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0.0,
|
||||
backgroundColor: Colors.transparent,
|
||||
leadingWidth: 30,
|
||||
title: InkWell(
|
||||
onTap: _addNewContacts,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.bug_report,
|
||||
color: Colors.grey,
|
||||
size: 22.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10.0),
|
||||
_contactsList.isEmpty
|
||||
? SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Text(
|
||||
"You don't have any conversation yet!",
|
||||
style: kMediumHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Click the icon below to add a contact',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
children: List.generate(
|
||||
_conversations.length,
|
||||
(index) => WidgetAnimator(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage:
|
||||
// ignore: avoid_dynamic_calls
|
||||
_conversations[index].photo == ''
|
||||
? const AssetImage('assets/dp.png')
|
||||
as ImageProvider
|
||||
: FileImage(
|
||||
// ignore: avoid_dynamic_calls
|
||||
File(_conversations[index].photo ??
|
||||
'')),
|
||||
),
|
||||
// ignore: avoid_dynamic_calls
|
||||
title: Text(_conversations[index].name ?? ''),
|
||||
subtitle:
|
||||
// ignore: avoid_dynamic_calls
|
||||
Text(_conversations[index].subtitle ?? ''),
|
||||
// ignore: avoid_dynamic_calls
|
||||
trailing: Icon(
|
||||
// ignore: avoid_dynamic_calls
|
||||
_conversations[index].isGroup
|
||||
? Icons.group
|
||||
: Icons.person,
|
||||
size: 18,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
onTap: () async {
|
||||
var value = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ConversationView(
|
||||
data: _conversations[index],
|
||||
),
|
||||
),
|
||||
);
|
||||
value ??= false;
|
||||
if (value) {
|
||||
_getContacts();
|
||||
_getGroups();
|
||||
}
|
||||
},
|
||||
onLongPress: () =>
|
||||
_conversationOptions(_conversations[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: PopupMenuButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
offset: const Offset(-10, -155),
|
||||
onSelected: (value) async {
|
||||
if (value == _options[0]) {
|
||||
var newMember =
|
||||
await Navigator.pushNamed(context, AppRoutes.scanInvitation);
|
||||
newMember ??= false;
|
||||
if (newMember == true) {
|
||||
_addNewMember();
|
||||
}
|
||||
} else if (value == _options[1]) {
|
||||
var newMember = await Navigator.pushNamed(context,
|
||||
kIsWeb ? AppRoutes.addContactWeb : AppRoutes.addContact);
|
||||
newMember ??= false;
|
||||
if (newMember == true) {
|
||||
_addNewMember();
|
||||
}
|
||||
} else {
|
||||
var newGroup =
|
||||
await Navigator.pushNamed(context, AppRoutes.addGroup);
|
||||
if (newGroup == true) {
|
||||
_getGroups();
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => _options
|
||||
.map(
|
||||
(opt) => PopupMenuItem(
|
||||
value: opt,
|
||||
child: Text(opt),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: const FloatingActionButton(
|
||||
heroTag: 'connect',
|
||||
onPressed: null,
|
||||
child: Icon(
|
||||
Icons.person_add,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// delete a contact
|
||||
void _deleteContact(Contact contact) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_contactsList.remove(contact);
|
||||
});
|
||||
await prefs.setString('contacts', Contact.encode(_contactsList));
|
||||
var snackBar = SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text('${contact.name} deleted!'),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
// delete a group
|
||||
void _deleteGroup(Group group) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_groupList.remove(group);
|
||||
});
|
||||
await prefs.setString('groups', Group.encode(_groupList));
|
||||
var snackBar = SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text('${group.name} deleted!'),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
void _conversationOptions(var chat) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_deleteConversation(chat);
|
||||
},
|
||||
child: const Text(
|
||||
'Delete Conversation',
|
||||
style: TextStyle(color: Colors.red),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_disconnect();
|
||||
},
|
||||
child: const Text('Disconnect')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteConversation(var chat) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Are you Sure?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'All conversation history will be deleted from your device!'),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _eraseMedia,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_eraseMedia = value;
|
||||
});
|
||||
}),
|
||||
const Text('Erase files & Media')
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
// ignore: avoid_dynamic_calls
|
||||
onTap: chat.isGroup
|
||||
? () {
|
||||
_deleteGroup(chat);
|
||||
_conversations.remove(chat);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
: () {
|
||||
_deleteContact(chat);
|
||||
_conversations.remove(chat);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.check, color: Colors.green),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.cancel_outlined, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _disconnect() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Text('Are you Sure?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Disconnecting will erase all the data from your device and you will no longer be able to contact again!'),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _eraseMedia,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_eraseMedia = value;
|
||||
});
|
||||
}),
|
||||
const Text('Erase files & Media')
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.check, color: Colors.green),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.cancel_outlined, color: Colors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// dummy ftn for loading new contacts
|
||||
void _addNewContacts() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// adding dummy contact
|
||||
List<Contact> _localList = [];
|
||||
final String? _local = prefs.getString('contacts');
|
||||
if (_local != null) {
|
||||
_localList = List.from(Contact.decode(_local));
|
||||
}
|
||||
|
||||
List<Contact> _newList = [
|
||||
Contact(
|
||||
name: 'Harry',
|
||||
subtitle: 'Hello!',
|
||||
),
|
||||
];
|
||||
_newList = _localList + _newList;
|
||||
|
||||
// dummy ftn for filling the list
|
||||
final String _newContacts = Contact.encode(_newList);
|
||||
|
||||
await prefs.setString('contacts', _newContacts);
|
||||
|
||||
// adding dummy contact
|
||||
List<Group> _localListGroup = [];
|
||||
final String? _localGroups = prefs.getString('groups');
|
||||
if (_local != null) {
|
||||
_localListGroup = List.from(Group.decode(_localGroups!));
|
||||
}
|
||||
|
||||
List<Group> _newGroups = [
|
||||
Group(
|
||||
name: 'College Friends',
|
||||
subtitle: 'Lovely people',
|
||||
members: ['Alice', 'James', 'Rio']),
|
||||
];
|
||||
_newGroups = _localListGroup + _newGroups;
|
||||
|
||||
// dummy ftn for filling the list
|
||||
final String _newGroup = Group.encode(_newGroups);
|
||||
|
||||
await prefs.setString('groups', _newGroup);
|
||||
|
||||
_getContacts();
|
||||
_getGroups();
|
||||
|
||||
const snackBar = SnackBar(
|
||||
backgroundColor: Colors.green,
|
||||
content: Text('New contacts loaded!'),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
|
||||
void _addNewMember() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// adding dummy contact
|
||||
List<Contact> _localList = [];
|
||||
final String? _local = prefs.getString('contacts');
|
||||
if (_local != null) {
|
||||
_localList = List.from(Contact.decode(_local));
|
||||
}
|
||||
|
||||
List<Contact> _newList = [
|
||||
Contact(
|
||||
name: 'Bob',
|
||||
subtitle: 'You and Bob are connected now!',
|
||||
),
|
||||
];
|
||||
_newList = _localList + _newList;
|
||||
|
||||
// dummy ftn for filling the list
|
||||
final String _newContacts = Contact.encode(_newList);
|
||||
|
||||
await prefs.setString('contacts', _newContacts);
|
||||
|
||||
_getContacts();
|
||||
_getGroups();
|
||||
|
||||
const snackBar = SnackBar(
|
||||
backgroundColor: Colors.green,
|
||||
content: Text('New connection added!'),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/model/contact.dart';
|
||||
|
||||
class ContactDetailsConversation extends StatelessWidget {
|
||||
final Contact contact;
|
||||
const ContactDetailsConversation({Key? key, required this.contact})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(contact.name!),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 65,
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
const Text('Display Name', style: kMediumHeadingStyle),
|
||||
Text(contact.name!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplex_chat/views/conversation/contact_detail_conversation.dart';
|
||||
import 'package:simplex_chat/views/conversation/group_detail_conversation.dart';
|
||||
import 'package:simplex_chat/widgets/message_bubble.dart';
|
||||
|
||||
/// View the details of group by tapping the [appBar]
|
||||
/// Viwe the details of chat by tapping the [appBar]
|
||||
/// Generate [Fake Messages] by tapping the [Message Icon]
|
||||
|
||||
class ConversationView extends StatefulWidget {
|
||||
// ignore: prefer_typing_uninitialized_variables
|
||||
final data;
|
||||
const ConversationView({Key? key, this.data}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ConversationViewState createState() => _ConversationViewState();
|
||||
}
|
||||
|
||||
class _ConversationViewState extends State<ConversationView> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _messageFieldController = TextEditingController();
|
||||
|
||||
FocusNode? _focus;
|
||||
bool _fieldEnabled = false;
|
||||
final List<Widget> _chatMessages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_focus = FocusNode();
|
||||
_focus!.addListener(() {
|
||||
debugPrint('FOCUS ${_focus!.hasFocus}');
|
||||
_fieldEnabled = _focus!.hasFocus;
|
||||
debugPrint('MESSAGE ENABLED $_fieldEnabled');
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageFieldController.dispose();
|
||||
_scrollController.dispose();
|
||||
_focus!.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
leadingWidth: MediaQuery.of(context).size.width * 0.085,
|
||||
title: InkWell(
|
||||
// ignore: avoid_dynamic_calls
|
||||
onTap: widget.data.isGroup
|
||||
? () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => GroupDetailsConversation(
|
||||
group: widget.data,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ContactDetailsConversation(
|
||||
contact: widget.data,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 15,
|
||||
// ignore: avoid_dynamic_calls
|
||||
backgroundImage: widget.data.photo == ''
|
||||
? const AssetImage('assets/dp.png') as ImageProvider
|
||||
: FileImage(
|
||||
// ignore: avoid_dynamic_calls
|
||||
File(widget.data.photo),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
Text(
|
||||
// ignore: avoid_dynamic_calls
|
||||
widget.data.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_chatMessages.add(MessageBubble(
|
||||
isUser: false,
|
||||
// ignore: avoid_dynamic_calls
|
||||
sender: widget.data.isGroup
|
||||
// ignore: avoid_dynamic_calls
|
||||
? widget.data.members.length == 0
|
||||
? 'some person'
|
||||
// ignore: avoid_dynamic_calls
|
||||
: widget.data.members[0]
|
||||
// ignore: avoid_dynamic_calls
|
||||
: widget.data.name,
|
||||
text: 'Hey there! How is it going?',
|
||||
));
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.bug_report,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
child: _chatMessages.isEmpty
|
||||
? const Center(
|
||||
child: Text('Send a message to get started'),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _chatMessages,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(width: 10.0),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 45.0,
|
||||
child: TextFormField(
|
||||
maxLines: null,
|
||||
focusNode: _focus,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.newline,
|
||||
controller: _messageFieldController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 10.0,
|
||||
),
|
||||
hintText: 'Message...',
|
||||
hintStyle: Theme.of(context).textTheme.caption,
|
||||
fillColor: Colors.grey[200],
|
||||
filled: true,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(360),
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(360),
|
||||
borderSide:
|
||||
const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (_messageFieldController.text.isNotEmpty) {
|
||||
setState(() {
|
||||
_chatMessages.add(MessageBubble(
|
||||
isUser: true,
|
||||
sender: 'You',
|
||||
text: _messageFieldController.text.trim(),
|
||||
));
|
||||
});
|
||||
_messageFieldController.clear();
|
||||
_focus!.unfocus();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send_rounded,
|
||||
size: 25.0, color: Colors.teal),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.02,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/model/contact.dart';
|
||||
import 'package:simplex_chat/model/group.dart';
|
||||
|
||||
enum MemberSetting { owner, admin, member }
|
||||
|
||||
class GroupDetailsConversation extends StatefulWidget {
|
||||
final Group group;
|
||||
const GroupDetailsConversation({Key? key, required this.group})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_GroupDetailsConversationState createState() =>
|
||||
_GroupDetailsConversationState();
|
||||
}
|
||||
|
||||
class _GroupDetailsConversationState extends State<GroupDetailsConversation> {
|
||||
MemberSetting _memberSetting = MemberSetting.member;
|
||||
|
||||
bool _addMember = false;
|
||||
List<Contact> _contactsList = []; // for storing contacts
|
||||
List _newMembers = [];
|
||||
|
||||
List<Group> _groupList = [];
|
||||
List _members = [];
|
||||
|
||||
// getting all members of group
|
||||
void _getMembers() {
|
||||
setState(() {
|
||||
_members = List.from(widget.group.members!);
|
||||
});
|
||||
}
|
||||
|
||||
// getting groups
|
||||
void _getGroups() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? _groups = prefs.getString('groups');
|
||||
if (_groups != null) {
|
||||
setState(() {
|
||||
_groupList = List.from(Group.decode(_groups));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// getting data from local storage FOR NOW
|
||||
void _getContacts() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? _contacts = prefs.getString('contacts');
|
||||
setState(() {
|
||||
_contactsList = List.from(Contact.decode(_contacts!));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getContacts();
|
||||
_getGroups();
|
||||
_getMembers();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(widget.group.name!),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: CircleAvatar(
|
||||
radius: 70,
|
||||
backgroundImage: widget.group.photo == ''
|
||||
? const AssetImage('assets/dp.png') as ImageProvider
|
||||
: FileImage(File(widget.group.photo!)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25.0),
|
||||
const Text('Group Name', style: kMediumHeadingStyle),
|
||||
Text(widget.group.name!),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add),
|
||||
title: const Text('Add a member'),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_addMember = !_addMember;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
_newMembers.isNotEmpty
|
||||
? const Text('New Members Added')
|
||||
: Container(),
|
||||
SizedBox(height: _newMembers.isNotEmpty ? 10.0 : 0.0),
|
||||
_newMembers.isNotEmpty
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
_newMembers.length,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_newMembers.remove(_newMembers[index]);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.grey.withAlpha(100)),
|
||||
child: Text(_newMembers[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(height: _addMember ? 10.0 : 0.0),
|
||||
_addMember ? const Text('Contacts Available') : Container(),
|
||||
SizedBox(height: _addMember ? 10.0 : 0.0),
|
||||
_addMember
|
||||
? ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
children: List.generate(
|
||||
_contactsList.length,
|
||||
(index) => ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
title: Text(_contactsList[index].name!),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_newMembers.add(_contactsList[index].name);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(height: _addMember ? 10.0 : 0.0),
|
||||
const Text('Members', style: kMediumHeadingStyle),
|
||||
const SizedBox(height: 5.0),
|
||||
for (int i = 0; i < _members.length; i++)
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
title: Text(_members[i]),
|
||||
trailing: InkWell(
|
||||
onTap: () => _memberSettings(_members[i]),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.settings),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: _newMembers.isEmpty
|
||||
? Container()
|
||||
: FloatingActionButton(
|
||||
onPressed: _addNewMembers,
|
||||
child: const Icon(Icons.check),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _memberSettings(String contact) {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
const Expanded(child: Text('Owner')),
|
||||
Radio(
|
||||
value: MemberSetting.owner,
|
||||
groupValue: _memberSetting,
|
||||
onChanged: (MemberSetting? value) {
|
||||
setState(() {
|
||||
_memberSetting = value!;
|
||||
});
|
||||
}),
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
const Expanded(child: Text('Admin')),
|
||||
Radio(
|
||||
groupValue: _memberSetting,
|
||||
value: MemberSetting.admin,
|
||||
onChanged: (MemberSetting? value) {
|
||||
setState(() {
|
||||
_memberSetting = value!;
|
||||
});
|
||||
}),
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
const Expanded(child: Text('Member')),
|
||||
Radio(
|
||||
groupValue: _memberSetting,
|
||||
value: MemberSetting.member,
|
||||
onChanged: (MemberSetting? value) {
|
||||
setState(() {
|
||||
_memberSetting = value!;
|
||||
});
|
||||
}),
|
||||
SizedBox(width: _size.width * 0.15),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () => _removeMember(contact),
|
||||
child: const Text('Remove from group',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addNewMembers() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// add new members to the _members list
|
||||
setState(() {
|
||||
_members = _members + _newMembers;
|
||||
_newMembers = [];
|
||||
_addMember = false;
|
||||
});
|
||||
|
||||
const snackBar = SnackBar(
|
||||
backgroundColor: Colors.green, content: Text('New members added!'));
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
|
||||
// get index of group in local
|
||||
int index = 0;
|
||||
|
||||
index = _groupList.indexWhere((group) => group.name == widget.group.name);
|
||||
|
||||
// add the full _members list to the group
|
||||
Group _updatedGroup = Group(
|
||||
name: widget.group.name,
|
||||
subtitle: widget.group.subtitle,
|
||||
members: _members,
|
||||
photo: widget.group.photo,
|
||||
);
|
||||
|
||||
// put it in updated group local list
|
||||
_groupList[index] = _updatedGroup;
|
||||
|
||||
// convert to string
|
||||
final String _updatedGroupString = Group.encode(_groupList);
|
||||
|
||||
// update the group in local storage
|
||||
await prefs.setString('groups', _updatedGroupString);
|
||||
}
|
||||
|
||||
void _removeMember(String contact) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
// remove the current member
|
||||
setState(() {
|
||||
_members.remove(contact);
|
||||
});
|
||||
|
||||
var snackBar = SnackBar(
|
||||
backgroundColor: Colors.red, content: Text('$contact removed!'));
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
|
||||
// get index of group in local
|
||||
int index = 0;
|
||||
|
||||
index = _groupList.indexWhere((group) => group.name == widget.group.name);
|
||||
|
||||
// new instance of group (updated)
|
||||
Group _updatedGroup = Group(
|
||||
name: widget.group.name,
|
||||
subtitle: widget.group.subtitle,
|
||||
photo: widget.group.photo,
|
||||
members: _members,
|
||||
);
|
||||
|
||||
// put it in group local list
|
||||
_groupList[index] = _updatedGroup;
|
||||
|
||||
// convert to string
|
||||
final String _updatedGroupString = Group.encode(_groupList);
|
||||
|
||||
// update the group in local storage
|
||||
await prefs.setString('groups', _updatedGroupString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/model/contact.dart';
|
||||
import 'package:simplex_chat/model/group.dart';
|
||||
import 'package:simplex_chat/widgets/custom_text_field.dart';
|
||||
|
||||
class AddGroupView extends StatefulWidget {
|
||||
const AddGroupView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AddGroupViewState createState() => _AddGroupViewState();
|
||||
}
|
||||
|
||||
class _AddGroupViewState extends State<AddGroupView> {
|
||||
// Image Picker --> DP properties
|
||||
final imgPicker = ImagePicker();
|
||||
File? image;
|
||||
String _groupPhotoPath = '';
|
||||
bool _uploading = false;
|
||||
bool _imageUploaded = false;
|
||||
|
||||
final List _members = [];
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _displayNameController = TextEditingController();
|
||||
final _descController = TextEditingController();
|
||||
|
||||
bool _addMember = false;
|
||||
List<Contact> _contactsList = []; // for storing contacts
|
||||
|
||||
// image buttons options
|
||||
final _dpBtnText = ['Remove', 'Gallery', 'Camera'];
|
||||
final _dpBtnColors = [Colors.red, Colors.purple, Colors.green];
|
||||
final _dpBtnIcons = [
|
||||
Icons.delete,
|
||||
Icons.photo_rounded,
|
||||
Icons.camera_alt_rounded
|
||||
];
|
||||
|
||||
// getting data from local storage FOR NOW
|
||||
void _getContacts() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? _contacts = prefs.getString('contacts');
|
||||
setState(() {
|
||||
_contactsList = List.from(Contact.decode(_contacts!));
|
||||
});
|
||||
}
|
||||
|
||||
String _userPhotoPath = '';
|
||||
void _getUserPhoto() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String? _photo = prefs.getString('photo${prefs.getString('displayName')}');
|
||||
if (_photo != null) {
|
||||
setState(() {
|
||||
_userPhotoPath = _photo;
|
||||
});
|
||||
}
|
||||
debugPrint(_userPhotoPath);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getUserPhoto();
|
||||
_getContacts();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_displayNameController.dispose();
|
||||
_descController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
title: const Text('New Group'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 180.0,
|
||||
width: 180.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
_imageUploaded
|
||||
? CircleAvatar(
|
||||
radius: _size.height * 0.12,
|
||||
backgroundImage:
|
||||
FileImage(File(_groupPhotoPath)),
|
||||
)
|
||||
: CircleAvatar(
|
||||
radius: _size.height * 0.12,
|
||||
backgroundImage:
|
||||
const AssetImage('assets/dp.png'),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: kSecondaryColor,
|
||||
elevation: 2.0,
|
||||
mini: true,
|
||||
onPressed: _updateProfilePic,
|
||||
child: _uploading
|
||||
? const SizedBox(
|
||||
height: 18.0,
|
||||
width: 18.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.add_a_photo,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _size.height * 0.03),
|
||||
const Text('Group Name', style: kSmallHeadingStyle),
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
CustomTextField(
|
||||
textEditingController: _displayNameController,
|
||||
textInputType: TextInputType.name,
|
||||
hintText: 'e.g College friends',
|
||||
validatorFtn: (String? value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Group name cannot be empty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
const Text('Group Description', style: kSmallHeadingStyle),
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
CustomTextField(
|
||||
textEditingController: _descController,
|
||||
textInputType: TextInputType.text,
|
||||
hintText: 'e.g Friends from UK',
|
||||
),
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
_members.isNotEmpty
|
||||
? const Text('Members Added')
|
||||
: Container(),
|
||||
SizedBox(height: _members.isNotEmpty ? 10.0 : 0.0),
|
||||
_members.isNotEmpty
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
_members.length,
|
||||
(index) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_members.remove(_members[index]);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(7.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
color: Colors.grey.withAlpha(100)),
|
||||
child: Text(_members[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
)
|
||||
: Container(),
|
||||
SizedBox(
|
||||
height: _members.isNotEmpty ? _size.height * 0.012 : 0.0),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add),
|
||||
title: const Text('Add a member'),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_addMember = !_addMember;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: _addMember ? _size.height * 0.012 : 0.0),
|
||||
_addMember ? const Text('Contacts Available') : Container(),
|
||||
SizedBox(height: _addMember ? _size.height * 0.012 : 0.0),
|
||||
_addMember
|
||||
? ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
children: List.generate(
|
||||
_contactsList.length,
|
||||
(index) => ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
title: Text(_contactsList[index].name!),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_members.add(_contactsList[index].name);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
Divider(height: _size.height * 0.035),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundImage: _userPhotoPath == ''
|
||||
? const AssetImage('assets/dp.png') as ImageProvider
|
||||
: FileImage(File(_userPhotoPath)),
|
||||
),
|
||||
title: const Text('You'),
|
||||
subtitle: const Text(
|
||||
'Owner',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12.0),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: Visibility(
|
||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'setup',
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_addNewGroup(_displayNameController.text.trim(),
|
||||
_descController.text.trim());
|
||||
_descController.clear();
|
||||
_displayNameController.clear();
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.check),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateProfilePic() {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(360.0)),
|
||||
height: 7.0,
|
||||
width: 50.0,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
' Profile photo',
|
||||
style: kHeadingStyle,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MaterialButton(
|
||||
color: _dpBtnColors.map((e) => e).elementAt(index),
|
||||
shape: const CircleBorder(),
|
||||
onPressed: index == 0
|
||||
? () => _removePic()
|
||||
: index == 1
|
||||
? () => _galleryPic()
|
||||
: () => _cameraPic(),
|
||||
child: Icon(
|
||||
_dpBtnIcons.map((e) => e).elementAt(index),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_dpBtnText.map((e) => e).elementAt(index),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _removePic() {
|
||||
setState(() {
|
||||
_imageUploaded = false;
|
||||
image = null;
|
||||
_groupPhotoPath = '';
|
||||
});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _cameraPic() async {
|
||||
try {
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from Camera
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_imageUploaded = true;
|
||||
_groupPhotoPath = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _galleryPic() async {
|
||||
try {
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from local storage
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_imageUploaded = true;
|
||||
_groupPhotoPath = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _addNewGroup(String name, String desc) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
List<Group> _localList = [];
|
||||
final String? _local = prefs.getString('groups');
|
||||
if (_local != null) {
|
||||
_localList = List.from(Group.decode(_local));
|
||||
}
|
||||
List<Group> _groups = [
|
||||
Group(
|
||||
name: name,
|
||||
photo: _groupPhotoPath,
|
||||
subtitle: desc,
|
||||
members: _members,
|
||||
),
|
||||
];
|
||||
debugPrint(_groups[0].isGroup.toString());
|
||||
_groups = _localList + _groups;
|
||||
|
||||
final String _newGroups = Group.encode(_groups);
|
||||
|
||||
await prefs.setString('groups', _newGroups);
|
||||
|
||||
var snackBar = SnackBar(
|
||||
backgroundColor: Colors.green,
|
||||
content: Text('$name added'),
|
||||
);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/providers/drawer_providers.dart';
|
||||
|
||||
class MyDrawer extends StatelessWidget {
|
||||
const MyDrawer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _drawerProviders = Provider.of<DrawerProvider>(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Builder(builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 30.0),
|
||||
SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
height: 55.0,
|
||||
),
|
||||
const Divider(height: 30.0),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.insert_invitation),
|
||||
title: const Text('Invitations'),
|
||||
onTap: () {
|
||||
_drawerProviders.currentIndex = 2;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh),
|
||||
title: const Text('Switch Profile'),
|
||||
subtitle: const Text(
|
||||
'Not supported yet!',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
onTap: () => _switchProfile(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// remove the locally stored user data
|
||||
void _switchProfile(BuildContext context) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
int _count = 0;
|
||||
Navigator.of(context).popUntil((route) => _count++ >= 2);
|
||||
String? _name = prefs.getString('displayName');
|
||||
await prefs.remove('displayName');
|
||||
await prefs.remove('fullName');
|
||||
await prefs.remove('photo$_name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/providers/drawer_providers.dart';
|
||||
import 'package:simplex_chat/views/contacts/conversations.dart';
|
||||
import 'package:simplex_chat/views/home/drawer.dart';
|
||||
import 'package:simplex_chat/views/invitations/invitation_view.dart';
|
||||
import 'package:simplex_chat/views/profile/profile_view.dart';
|
||||
|
||||
/// Generate [Fake Contacts] by tapping the [Bug Icon]
|
||||
|
||||
class HomeView extends StatefulWidget {
|
||||
const HomeView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomeViewState createState() => _HomeViewState();
|
||||
}
|
||||
|
||||
class _HomeViewState extends State<HomeView> {
|
||||
String? _photo = '';
|
||||
String? _displayName = '';
|
||||
|
||||
// views
|
||||
final List<Widget> _views = [
|
||||
const ProfileView(),
|
||||
const Conversations(),
|
||||
const Invitations(),
|
||||
];
|
||||
|
||||
void _getUserData() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_displayName = prefs.getString('displayName');
|
||||
_photo = prefs.getString('photo$_displayName');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getUserData();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _drawerProviders = Provider.of<DrawerProvider>(context);
|
||||
return WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
child: Scaffold(
|
||||
drawer: const Drawer(child: MyDrawer()),
|
||||
body: SafeArea(
|
||||
child: Builder(builder: (context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_views[_drawerProviders.currentIndex],
|
||||
_drawerProviders.currentIndex == 0
|
||||
? Positioned(
|
||||
top: MediaQuery.of(context).size.height * 0.017,
|
||||
left: MediaQuery.of(context).size.width * 0.02,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_drawerProviders.currentIndex = 1;
|
||||
_getUserData();
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.arrow_back, color: kPrimaryColor),
|
||||
),
|
||||
),
|
||||
)
|
||||
: _drawerProviders.currentIndex == 2
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
_drawerProviders.currentIndex = 1;
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.arrow_back,
|
||||
color: kPrimaryColor)),
|
||||
),
|
||||
const Spacer(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Hi! $_displayName',
|
||||
style: kSmallHeadingStyle),
|
||||
const Text('Good day!'),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_drawerProviders.currentIndex = 0;
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundImage: _photo!.isEmpty
|
||||
? const AssetImage('assets/dp.png')
|
||||
as ImageProvider
|
||||
: FileImage(File(_photo!)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SvgPicture.asset('assets/menu.svg'),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Hi! $_displayName',
|
||||
style: kSmallHeadingStyle),
|
||||
const Text('Good day!'),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_drawerProviders.currentIndex = 0;
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundImage: _photo!.isEmpty
|
||||
? const AssetImage('assets/dp.png')
|
||||
as ImageProvider
|
||||
: FileImage(File(_photo!)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
return (await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Exit Application', style: kMediumHeadingStyle),
|
||||
content: const Text('Are You Sure?'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text(
|
||||
'Yes',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () => SystemNavigator.pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('No'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)) ??
|
||||
false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
|
||||
class Invitations extends StatefulWidget {
|
||||
const Invitations({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Invitations> createState() => _InvitationsState();
|
||||
}
|
||||
|
||||
class _InvitationsState extends State<Invitations> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 40.0),
|
||||
Row(
|
||||
children: const [
|
||||
Icon(Icons.inventory_outlined, color: kPrimaryColor),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Invitations',
|
||||
style: kHeadingStyle,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"You don't have any invitations yet!",
|
||||
style: kSmallHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:simplex_chat/app_routes.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
|
||||
class IntroView extends StatelessWidget {
|
||||
const IntroView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
height: 80.0,
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
|
||||
const Text(
|
||||
'Complete your profile to begin using SimpleX. Your profile is local to your device and will help identify you to your connections.',
|
||||
style: kMediumHeadingStyle,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'welcome',
|
||||
onPressed: () => Navigator.pushNamed(context, AppRoutes.setupProfile),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/widgets/custom_text_field.dart';
|
||||
|
||||
class ProfileView extends StatefulWidget {
|
||||
const ProfileView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ProfileViewState createState() => _ProfileViewState();
|
||||
}
|
||||
|
||||
class _ProfileViewState extends State<ProfileView> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
// controllers
|
||||
final TextEditingController _displayNameController = TextEditingController();
|
||||
final TextEditingController _fullNameController = TextEditingController();
|
||||
|
||||
// Image Picker --> DP properties
|
||||
final imgPicker = ImagePicker();
|
||||
File? image;
|
||||
String? _photo = '';
|
||||
bool _uploading = false;
|
||||
|
||||
// image buttons options
|
||||
final _dpBtnText = ['Remove', 'Gallery', 'Camera'];
|
||||
final _dpBtnColors = [Colors.red, Colors.purple, Colors.green];
|
||||
final _dpBtnIcons = [
|
||||
Icons.delete,
|
||||
Icons.photo_rounded,
|
||||
Icons.camera_alt_rounded
|
||||
];
|
||||
|
||||
String? _displayName = '';
|
||||
String? _fullName = '';
|
||||
|
||||
void _getUserData() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_fullName = prefs.getString('fullName');
|
||||
_displayName = prefs.getString('displayName');
|
||||
_photo = prefs.getString('photo$_displayName');
|
||||
});
|
||||
_displayNameController.text = _displayName!;
|
||||
if (_fullName != null) _fullNameController.text = _fullName!;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getUserData();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_displayNameController.dispose();
|
||||
_fullNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: _size.height * 0.05),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 180.0,
|
||||
width: 180.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
_photo != ''
|
||||
? CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 100.0,
|
||||
backgroundImage: FileImage(File(_photo!)),
|
||||
)
|
||||
: const CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 100.0,
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: kSecondaryColor,
|
||||
elevation: 2.0,
|
||||
mini: true,
|
||||
onPressed: _updateProfilePic,
|
||||
child: _uploading
|
||||
? const SizedBox(
|
||||
height: 18.0,
|
||||
width: 18.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.add_a_photo,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
SizedBox(height: _size.height * 0.035),
|
||||
const Text('Display Name', style: kSmallHeadingStyle),
|
||||
const SizedBox(height: 10.0),
|
||||
CustomTextField(
|
||||
textEditingController: _displayNameController,
|
||||
textInputType: TextInputType.name,
|
||||
hintText: 'e.g John',
|
||||
validatorFtn: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Display name cannot be empty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: _size.height * 0.035),
|
||||
const Text('Full Name', style: kSmallHeadingStyle),
|
||||
SizedBox(height: _size.height * 0.012),
|
||||
CustomTextField(
|
||||
textEditingController: _fullNameController,
|
||||
textInputType: TextInputType.name,
|
||||
hintText: 'e.g John Doe',
|
||||
),
|
||||
SizedBox(height: _size.height * 0.035),
|
||||
const Text(
|
||||
'Your display name is what your contact will know you :)',
|
||||
style: TextStyle(letterSpacing: 1.2),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'save',
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
FocusManager.instance.primaryFocus!.unfocus();
|
||||
await _createProfile();
|
||||
const snackBar = SnackBar(
|
||||
backgroundColor: Colors.green,
|
||||
content: Text('Profile updated!'),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.check),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// create profile and store in local
|
||||
Future<void> _createProfile() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('displayName', _displayNameController.text.trim());
|
||||
await prefs.setString('fullName', _fullNameController.text.trim());
|
||||
await prefs.setString(
|
||||
'photo${_displayNameController.text.trim()}', _photo!);
|
||||
|
||||
debugPrint(prefs.getString('photo'));
|
||||
}
|
||||
|
||||
void _updateProfilePic() {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(360.0)),
|
||||
height: 7.0,
|
||||
width: 50.0,
|
||||
),
|
||||
SizedBox(height: _size.height * 0.025),
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
' Profile photo',
|
||||
style: kHeadingStyle,
|
||||
),
|
||||
),
|
||||
SizedBox(height: _size.height * 0.015),
|
||||
Row(
|
||||
children: List.generate(
|
||||
3,
|
||||
(index) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MaterialButton(
|
||||
color: _dpBtnColors.map((e) => e).elementAt(index),
|
||||
shape: const CircleBorder(),
|
||||
onPressed: index == 0
|
||||
? () => _removePic()
|
||||
: index == 1
|
||||
? () => _galleryPic()
|
||||
: () => _cameraPic(),
|
||||
child: Icon(
|
||||
_dpBtnIcons.map((e) => e).elementAt(index),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_dpBtnText.map((e) => e).elementAt(index),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _removePic() {
|
||||
setState(() {
|
||||
image = null;
|
||||
_photo = '';
|
||||
});
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _cameraPic() async {
|
||||
try {
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from Camera
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_photo = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _galleryPic() async {
|
||||
try {
|
||||
debugPrint('gallery pic');
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from local storage
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_photo = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share/share.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/widgets/custom_btn.dart';
|
||||
|
||||
class ScanInvitationView extends StatelessWidget {
|
||||
const ScanInvitationView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: const Text('Share QR Code'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Display this QR Code for your contact to scan.',
|
||||
style: kMediumHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: _size.height * 0.04),
|
||||
GestureDetector(
|
||||
onTap: () => _showConnection(context),
|
||||
child:
|
||||
Image.asset('assets/code.png', height: _size.height * 0.3),
|
||||
),
|
||||
SizedBox(height: _size.height * 0.04),
|
||||
const Text(
|
||||
'If you cannot share your QR Code, send the invitation via a trusted method.',
|
||||
style: kMediumHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: _size.height * 0.04),
|
||||
CustomButton(
|
||||
width: _size.width * 0.5,
|
||||
height: _size.height * 0.055,
|
||||
onPressed: _shareLink,
|
||||
color: kPrimaryColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.share, color: Colors.white),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
'Share',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConnection(BuildContext context) {
|
||||
Size _size = MediaQuery.of(context).size;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage: const AssetImage('assets/dp.png'),
|
||||
radius: _size.height * 0.085,
|
||||
),
|
||||
SizedBox(height: _size.height * 0.045),
|
||||
const Text(
|
||||
'Bob wants to connect with you!',
|
||||
style: kMediumHeadingStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: _size.height * 0.045),
|
||||
CustomButton(
|
||||
width: _size.width * 0.5,
|
||||
height: _size.height * 0.055,
|
||||
onPressed: () {
|
||||
// work around for now
|
||||
Navigator.pop(context);
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
color: kPrimaryColor,
|
||||
child: const Text(
|
||||
'Confirm',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _size.height * 0.013),
|
||||
CustomButton(
|
||||
width: _size.width * 0.5,
|
||||
height: _size.height * 0.055,
|
||||
onPressed: () {
|
||||
int _count = 0;
|
||||
Navigator.popUntil(context, (route) => _count++ >= 2);
|
||||
},
|
||||
color: kSecondaryColor,
|
||||
child: const Text(
|
||||
'Ignore',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _shareLink() {
|
||||
Share.share('Add me to SimpleX Chat via link https://someLinkHere.io');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplex_chat/app_routes.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
import 'package:simplex_chat/views/home/home_view.dart';
|
||||
import 'package:simplex_chat/widgets/custom_text_field.dart';
|
||||
|
||||
class SetupProfileView extends StatefulWidget {
|
||||
const SetupProfileView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SetupProfileViewState createState() => _SetupProfileViewState();
|
||||
}
|
||||
|
||||
class _SetupProfileViewState extends State<SetupProfileView> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
// controllers
|
||||
final TextEditingController _displayNameController = TextEditingController();
|
||||
final TextEditingController _fullNameController = TextEditingController();
|
||||
|
||||
// Image Picker --> DP properties
|
||||
final imgPicker = ImagePicker();
|
||||
File? image;
|
||||
String photoUrl = '';
|
||||
bool _uploading = false;
|
||||
bool _imageUploaded = false;
|
||||
|
||||
// image buttons options
|
||||
final _dpBtnText = ['Gallery', 'Camera'];
|
||||
final _dpBtnColors = [Colors.purple, Colors.green];
|
||||
final _dpBtnIcons = [Icons.photo_rounded, Icons.camera_alt_rounded];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_displayNameController.dispose();
|
||||
_fullNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 180.0,
|
||||
width: 180.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
_imageUploaded
|
||||
? CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 100.0,
|
||||
backgroundImage: FileImage(image!),
|
||||
)
|
||||
: const CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 100.0,
|
||||
backgroundImage: AssetImage('assets/dp.png'),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: kSecondaryColor,
|
||||
elevation: 2.0,
|
||||
mini: true,
|
||||
onPressed: _updateProfilePic,
|
||||
child: _uploading
|
||||
? const SizedBox(
|
||||
height: 18.0,
|
||||
width: 18.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.add_a_photo,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 25.0),
|
||||
const Text('Display Name', style: kSmallHeadingStyle),
|
||||
const SizedBox(height: 10.0),
|
||||
CustomTextField(
|
||||
textEditingController: _displayNameController,
|
||||
textInputType: TextInputType.name,
|
||||
hintText: 'e.g John',
|
||||
validatorFtn: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Display name cannot be empty';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25.0),
|
||||
const Text('Full Name', style: kSmallHeadingStyle),
|
||||
const SizedBox(height: 10.0),
|
||||
CustomTextField(
|
||||
textEditingController: _fullNameController,
|
||||
textInputType: TextInputType.name,
|
||||
hintText: 'e.g John Doe',
|
||||
),
|
||||
const SizedBox(height: 25.0),
|
||||
const Text(
|
||||
'Your display name is what your contact will know you :)',
|
||||
style: TextStyle(letterSpacing: 1.2),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: Visibility(
|
||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'setup',
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
await _createProfile();
|
||||
|
||||
await Navigator.pushNamed(context, AppRoutes.home);
|
||||
|
||||
_displayNameController.clear();
|
||||
_fullNameController.clear();
|
||||
image = null;
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.check),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// create profile and save the data locally to access across the app
|
||||
Future<void> _createProfile() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('displayName', _displayNameController.text.trim());
|
||||
await prefs.setString('fullName', _fullNameController.text.trim());
|
||||
await prefs.setString(
|
||||
'photo${_displayNameController.text.trim()}', photoUrl);
|
||||
|
||||
debugPrint(prefs.getString('photo'));
|
||||
}
|
||||
|
||||
void _updateProfilePic() {
|
||||
showModalBottomSheet(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(360.0)),
|
||||
height: 7.0,
|
||||
width: 50.0,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
' Profile photo',
|
||||
style: kHeadingStyle,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
Row(
|
||||
children: List.generate(
|
||||
2,
|
||||
(index) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MaterialButton(
|
||||
color: _dpBtnColors.map((e) => e).elementAt(index),
|
||||
shape: const CircleBorder(),
|
||||
onPressed:
|
||||
index == 0 ? () => _galleryPic() : () => _cameraPic(),
|
||||
child: Icon(
|
||||
_dpBtnIcons.map((e) => e).elementAt(index),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_dpBtnText.map((e) => e).elementAt(index),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _cameraPic() async {
|
||||
try {
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from Camera
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.camera,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_imageUploaded = true;
|
||||
photoUrl = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _galleryPic() async {
|
||||
try {
|
||||
debugPrint('gallery pic');
|
||||
setState(() {
|
||||
_uploading = true;
|
||||
});
|
||||
|
||||
// picking Image from local storage
|
||||
final file = await imgPicker.getImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
|
||||
if (file != null) {
|
||||
image = File(file.path);
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
_imageUploaded = true;
|
||||
photoUrl = file.path;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_uploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:progress_indicators/progress_indicators.dart';
|
||||
import 'package:simplex_chat/animations/entrance_fader.dart';
|
||||
import 'package:simplex_chat/app_routes.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SplashScreenState createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
// delay on splash screen
|
||||
void _splashDelay() {
|
||||
Future.delayed(const Duration(seconds: 4), () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
AppRoutes.intro,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_splashDelay();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
EntranceFader(
|
||||
duration: const Duration(seconds: 1),
|
||||
offset: const Offset(0, 15),
|
||||
child: SvgPicture.asset('assets/logo.svg', height: 85),
|
||||
),
|
||||
EntranceFader(
|
||||
offset: const Offset(0, 0),
|
||||
duration: const Duration(seconds: 1),
|
||||
delay: const Duration(seconds: 1),
|
||||
child: JumpingDotsProgressIndicator(
|
||||
fontSize: 40,
|
||||
color: kPrimaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomButton extends StatelessWidget {
|
||||
const CustomButton(
|
||||
{required this.width,
|
||||
required this.height,
|
||||
required this.onPressed,
|
||||
required this.color,
|
||||
required this.child,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
final double width;
|
||||
final double height;
|
||||
final void Function() onPressed;
|
||||
final Widget child;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: MaterialButton(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5.0)),
|
||||
color: color,
|
||||
onPressed: onPressed,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomTextField extends StatefulWidget {
|
||||
final TextEditingController textEditingController;
|
||||
final TextInputType textInputType;
|
||||
|
||||
final String hintText;
|
||||
|
||||
final String? Function(String?)? validatorFtn;
|
||||
|
||||
const CustomTextField({
|
||||
Key? key,
|
||||
required this.textEditingController,
|
||||
required this.textInputType,
|
||||
required this.hintText,
|
||||
this.validatorFtn,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CustomTextFieldState createState() => _CustomTextFieldState();
|
||||
}
|
||||
|
||||
class _CustomTextFieldState extends State<CustomTextField> {
|
||||
final FocusNode _node = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_node.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double width = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
width: width * 0.89,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8.0)),
|
||||
child: TextFormField(
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: widget.textEditingController,
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: widget.textInputType,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
hintText: widget.hintText,
|
||||
hintStyle: Theme.of(context).textTheme.caption,
|
||||
fillColor: Colors.grey[200],
|
||||
filled: true,
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent),
|
||||
),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
focusedErrorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
),
|
||||
validator: widget.validatorFtn,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplex_chat/constants.dart';
|
||||
|
||||
class MessageBubble extends StatefulWidget {
|
||||
const MessageBubble({
|
||||
Key? key,
|
||||
required this.sender,
|
||||
required this.text,
|
||||
required this.isUser,
|
||||
}) : super(key: key);
|
||||
final String sender;
|
||||
final String text;
|
||||
final bool isUser;
|
||||
|
||||
@override
|
||||
_MessageBubbleState createState() => _MessageBubbleState();
|
||||
}
|
||||
|
||||
class _MessageBubbleState extends State<MessageBubble> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
widget.isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.sender,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 2.0),
|
||||
Material(
|
||||
borderRadius: widget.isUser
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10))
|
||||
: const BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10)),
|
||||
elevation: 1.0,
|
||||
color: widget.isUser ? Colors.teal : kPrimaryColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||
child: Text(
|
||||
widget.text,
|
||||
style: const TextStyle(fontSize: 15, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,20 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -43,6 +57,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1+5"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -57,11 +85,39 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.2"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -69,11 +125,79 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.22.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.3"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.5+4"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -95,6 +219,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -102,6 +240,146 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
progress_indicators:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: progress_indicators
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
qr_code_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr_code_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
share:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -156,6 +434,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
url_strategy:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: url_strategy
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -163,5 +448,34 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.2"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.14.0 <3.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: simplex_app
|
||||
name: simplex_chat
|
||||
description: A new Flutter project.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
@@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.14.0 <3.0.0"
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@@ -34,6 +34,24 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
progress_indicators: ^1.0.0
|
||||
|
||||
# QR Code
|
||||
qr_code_scanner: ^0.5.2
|
||||
|
||||
# for placing svg
|
||||
flutter_svg: ^0.22.0
|
||||
|
||||
# attachments
|
||||
image_picker: ^0.7.5+3
|
||||
file_picker: ^3.0.2+2
|
||||
|
||||
# state management
|
||||
provider: ^6.0.1
|
||||
shared_preferences: ^2.0.8
|
||||
|
||||
# share
|
||||
share: ^2.0.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -45,6 +63,13 @@ dev_dependencies:
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
flutter_launcher_icons: ^0.9.2
|
||||
url_strategy: ^0.2.0
|
||||
|
||||
flutter_icons:
|
||||
image_path: "assets/x.png"
|
||||
android: true
|
||||
ios: true
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@@ -58,8 +83,8 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
assets:
|
||||
- assets/
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
|
||||
@@ -5,26 +5,26 @@
|
||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||
// tree, read text, and verify that the values of widget properties are correct.
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import "package:simplex_app/main.dart";
|
||||
import 'package:simplex_chat/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const MyApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text("0"), findsOneWidget);
|
||||
expect(find.text("1"), findsNothing);
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsNothing);
|
||||
|
||||
// Tap the '+' icon and trigger a frame.
|
||||
await tester.tap(find.byIcon(Icons.add));
|
||||
await tester.pump();
|
||||
|
||||
// Verify that our counter has incremented.
|
||||
expect(find.text("0"), findsNothing);
|
||||
expect(find.text("1"), findsOneWidget);
|
||||
});
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
}, skip: true);
|
||||
}
|
||||
|
||||
@@ -97,5 +97,6 @@
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.3.1/dist/jsQR.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
void main() {}
|
||||
// import 'package:simplexmq/simplexmq.dart';
|
||||
|
||||
// void main() {
|
||||
// var awesome = Awesome();
|
||||
// print('awesome: ${awesome.isAwesome}');
|
||||
// }
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
library simplexmq;
|
||||
|
||||
export 'src/protocol.dart';
|
||||
export 'src/transport.dart' show Transport, SMPTransportClient;
|
||||
export 'src/transport.dart' show Transport;
|
||||
|
||||
@@ -130,25 +130,3 @@ Uint8List? decode64(Uint8List b64) {
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Uint8List encodeInt32(int n) {
|
||||
final data = Uint8List(4);
|
||||
ByteData.sublistView(data).setInt32(0, n);
|
||||
return data;
|
||||
}
|
||||
|
||||
Uint8List encodeInt16(int n) {
|
||||
final data = Uint8List(2);
|
||||
ByteData.sublistView(data).setInt16(0, n);
|
||||
return data;
|
||||
}
|
||||
|
||||
extension EqualUint8List on Uint8List {
|
||||
bool equal(Uint8List b) {
|
||||
if (length != b.length) return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (this[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'dart:math' show Random;
|
||||
import 'dart:typed_data';
|
||||
import 'package:pointycastle/api.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart';
|
||||
import 'package:pointycastle/asymmetric/oaep.dart';
|
||||
import 'package:pointycastle/asymmetric/rsa.dart';
|
||||
import 'package:pointycastle/block/aes_fast.dart';
|
||||
import 'package:pointycastle/block/modes/gcm.dart';
|
||||
import 'package:pointycastle/digests/sha256.dart';
|
||||
import 'package:pointycastle/key_generators/api.dart';
|
||||
import 'package:pointycastle/key_generators/rsa_key_generator.dart';
|
||||
import 'package:pointycastle/random/fortuna_random.dart';
|
||||
import 'package:pointycastle/signers/pss_signer.dart';
|
||||
import 'buffer.dart';
|
||||
|
||||
class AESKey {
|
||||
final Uint8List _key;
|
||||
AESKey._make(this._key);
|
||||
|
||||
static AESKey random([bool secure = false]) =>
|
||||
AESKey._make((secure ? secureRandomBytes : pseudoRandomBytes)(32));
|
||||
|
||||
static AESKey decode(Uint8List rawKey) => AESKey._make(rawKey);
|
||||
|
||||
Uint8List get bytes => _key;
|
||||
}
|
||||
|
||||
Uint8List randomIV() => pseudoRandomBytes(16);
|
||||
|
||||
Uint8List secureRandomBytes(int len) => _randomBytes(len, Random.secure());
|
||||
|
||||
final sessionSeed = Random.secure();
|
||||
|
||||
Uint8List pseudoRandomBytes(int len) => _randomBytes(len, sessionSeed);
|
||||
|
||||
Uint8List _randomBytes(int len, Random seedSource) {
|
||||
final bytes = Uint8List(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
bytes[i] = seedSource.nextInt(256);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
final paddingByte = '#'.codeUnitAt(0);
|
||||
|
||||
Uint8List encryptAES(AESKey key, Uint8List iv, int padTo, Uint8List data) {
|
||||
if (data.length >= padTo) throw ArgumentError('large message');
|
||||
final padded = Uint8List(padTo);
|
||||
padded.setAll(0, data);
|
||||
padded.fillRange(data.length, padTo, paddingByte);
|
||||
return _makeGCMCipher(key, iv, true).process(padded);
|
||||
}
|
||||
|
||||
Uint8List decryptAES(AESKey key, Uint8List iv, Uint8List encryptedAndTag) =>
|
||||
_makeGCMCipher(key, iv, false).process(encryptedAndTag);
|
||||
|
||||
GCMBlockCipher _makeGCMCipher(AESKey key, Uint8List iv, bool encrypt) =>
|
||||
GCMBlockCipher(AESFastEngine())
|
||||
..init(encrypt, AEADParameters(KeyParameter(key._key), 128, iv, empty));
|
||||
|
||||
FortunaRandom _secureFortunaRandom() =>
|
||||
FortunaRandom()..seed(KeyParameter(secureRandomBytes(32)));
|
||||
|
||||
AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey> generateRSAkeyPair(
|
||||
[int bitLength = 2048]) {
|
||||
final keyGen = RSAKeyGenerator()
|
||||
..init(ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64),
|
||||
_secureFortunaRandom()));
|
||||
final pair = keyGen.generateKeyPair();
|
||||
return AsymmetricKeyPair<RSAPublicKey, RSAPrivateKey>(
|
||||
pair.publicKey as RSAPublicKey, pair.privateKey as RSAPrivateKey);
|
||||
}
|
||||
|
||||
Uint8List encryptOAEP(RSAPublicKey key, Uint8List data) =>
|
||||
_oaep(true, PublicKeyParameter<RSAPublicKey>(key)).process(data);
|
||||
|
||||
Uint8List decryptOAEP(RSAPrivateKey key, Uint8List data) =>
|
||||
_oaep(false, PrivateKeyParameter<RSAPrivateKey>(key)).process(data);
|
||||
|
||||
OAEPEncoding _oaep(bool encrypt, AsymmetricKeyParameter keyParam) =>
|
||||
OAEPEncoding.withSHA256(RSAEngine())..init(encrypt, keyParam);
|
||||
|
||||
Uint8List signPSS(RSAPrivateKey privateKey, Uint8List data) =>
|
||||
_pss(true, PrivateKeyParameter<RSAPrivateKey>(privateKey))
|
||||
.generateSignature(data)
|
||||
.bytes;
|
||||
|
||||
bool verifyPSS(RSAPublicKey publicKey, Uint8List data, Uint8List sig) {
|
||||
try {
|
||||
return _pss(false, PublicKeyParameter<RSAPublicKey>(publicKey))
|
||||
.verifySignature(data, PSSSignature(sig));
|
||||
} on ArgumentError {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PSSSigner _pss(bool sign, AsymmetricKeyParameter keyParam) => PSSSigner(
|
||||
RSAEngine(), SHA256Digest(), SHA256Digest())
|
||||
..init(sign,
|
||||
ParametersWithSaltConfiguration(keyParam, _secureFortunaRandom(), 32));
|
||||
@@ -13,11 +13,9 @@ final charUpperA = cc('A');
|
||||
final charUpperZ = cc('Z');
|
||||
final charPlus = cc('+');
|
||||
final charSlash = cc('/');
|
||||
final charDot = cc('.');
|
||||
|
||||
class Parser {
|
||||
final Uint8List _s;
|
||||
final List<int> _positions = [];
|
||||
int _pos = 0;
|
||||
bool _fail = false;
|
||||
Parser(this._s);
|
||||
@@ -36,20 +34,6 @@ class Parser {
|
||||
return res;
|
||||
}
|
||||
|
||||
T? tryParse<T>(T? Function(Parser p) parse) {
|
||||
if (_fail || _pos >= _s.length) {
|
||||
_fail = true;
|
||||
return null;
|
||||
}
|
||||
_positions.add(_pos);
|
||||
final res = parse(this);
|
||||
final prevPos = _positions.removeLast();
|
||||
if (res == null) {
|
||||
_pos = prevPos;
|
||||
_fail = false;
|
||||
}
|
||||
}
|
||||
|
||||
// takes a required number of bytes
|
||||
Uint8List? take(int len) => _run(() {
|
||||
final end = _pos + len;
|
||||
@@ -86,14 +70,6 @@ class Parser {
|
||||
return true;
|
||||
});
|
||||
|
||||
// takes the passed char
|
||||
bool? char(int c) => _run(() {
|
||||
if (_s[_pos] == c) {
|
||||
_pos++;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// takes space
|
||||
bool? space() => _run(() {
|
||||
if (_s[_pos] == charSpace) {
|
||||
|
||||
@@ -174,7 +174,7 @@ class ERR extends BrokerCommand {
|
||||
: cmdErr = err == ErrorType.CMD
|
||||
? throw ArgumentError('CMD error should be created with ERR.CMD')
|
||||
: null;
|
||||
ERR.cmd(CmdErrorType this.cmdErr) : err = ErrorType.CMD;
|
||||
ERR.cmd(this.cmdErr) : err = ErrorType.CMD;
|
||||
@override
|
||||
Uint8List serialize() {
|
||||
final _err = errorTags[err]!;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:asn1lib/asn1lib.dart';
|
||||
import 'package:pointycastle/asymmetric/api.dart'
|
||||
show RSAPublicKey, RSAPrivateKey;
|
||||
|
||||
const _rsaOid = '1.2.840.113549.1.1.1';
|
||||
final _asnRsaOid = ASN1ObjectIdentifier.fromComponentString(_rsaOid);
|
||||
final _asn1Null = ASN1Null();
|
||||
|
||||
ASN1Sequence _asn1Sequence(List<ASN1Object> elements) {
|
||||
final seq = ASN1Sequence()..elements = elements;
|
||||
return seq;
|
||||
}
|
||||
|
||||
void _assertRsaAlgorithm(ASN1Sequence seq) {
|
||||
if (seq.elements.isEmpty ||
|
||||
seq.elements[0] is! ASN1ObjectIdentifier ||
|
||||
(seq.elements[0] as ASN1ObjectIdentifier).identifier != _rsaOid) {
|
||||
throw Exception('Invalid key algorithm identifier');
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes binary PKCS1 to [RSAPublicKey]
|
||||
RSAPublicKey decodeRsaPubKeyPKCS1(Uint8List bytes) {
|
||||
final els = ASN1Sequence.fromBytes(bytes).elements;
|
||||
if (els.length != 2 || els[0] is! ASN1Integer || els[1] is! ASN1Integer) {
|
||||
throw Exception('Invalid PKCS1 encoding');
|
||||
}
|
||||
return RSAPublicKey(
|
||||
(els[0] as ASN1Integer).valueAsBigInteger!,
|
||||
(els[1] as ASN1Integer).valueAsBigInteger!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Decodes binary SPKI to [RSAPublicKey]
|
||||
RSAPublicKey decodeRsaPubKey(Uint8List bytes) {
|
||||
final els = ASN1Sequence.fromBytes(bytes).elements;
|
||||
if (els.length != 2 || els[1] is! ASN1BitString || els[0] is! ASN1Sequence) {
|
||||
throw Exception('Invalid SPKI structure');
|
||||
}
|
||||
_assertRsaAlgorithm(els[0] as ASN1Sequence);
|
||||
return decodeRsaPubKeyPKCS1(els[1].valueBytes().sublist(1));
|
||||
}
|
||||
|
||||
/// Encodes [key] as binary PKCS1
|
||||
Uint8List encodeRsaPubKeyPKCS1(RSAPublicKey key) =>
|
||||
_asn1Sequence([ASN1Integer(key.modulus!), ASN1Integer(key.publicExponent!)])
|
||||
.encodedBytes;
|
||||
|
||||
/// Encodes [key] as binary SPKI
|
||||
Uint8List encodeRsaPubKey(RSAPublicKey key) => _asn1Sequence([
|
||||
_asn1Sequence([_asnRsaOid, _asn1Null]),
|
||||
ASN1BitString(encodeRsaPubKeyPKCS1(key))
|
||||
]).encodedBytes;
|
||||
|
||||
/// Decodes binary PKCS1 to [RSAPrivateKey]
|
||||
RSAPrivateKey decodeRsaPrivKeyPKCS1(Uint8List bytes) {
|
||||
final els = ASN1Sequence.fromBytes(bytes).elements;
|
||||
if (els.length != 9 || els.any((el) => el is! ASN1Integer)) {
|
||||
throw Exception('Invalid PKCS1 encoding');
|
||||
}
|
||||
return RSAPrivateKey(
|
||||
(els[1] as ASN1Integer).valueAsBigInteger!,
|
||||
(els[3] as ASN1Integer).valueAsBigInteger!,
|
||||
(els[4] as ASN1Integer).valueAsBigInteger!,
|
||||
(els[5] as ASN1Integer).valueAsBigInteger!);
|
||||
}
|
||||
|
||||
/// Decodes binary PKCS8 to [RSAPrivateKey]
|
||||
RSAPrivateKey decodeRsaPrivKey(Uint8List bytes) {
|
||||
final els = ASN1Sequence.fromBytes(bytes).elements;
|
||||
if (els.length != 3 ||
|
||||
els[1] is! ASN1Sequence ||
|
||||
els[2] is! ASN1OctetString) {
|
||||
throw Exception('Invalid PKCS8 structure');
|
||||
}
|
||||
_assertRsaAlgorithm(els[1] as ASN1Sequence);
|
||||
return decodeRsaPrivKeyPKCS1(els[2].valueBytes());
|
||||
}
|
||||
|
||||
final _asnZero = ASN1Integer(BigInt.from(0));
|
||||
|
||||
/// Encodes [key] as PKCS1 binary
|
||||
Uint8List encodeRsaPrivKeyPKCS1(RSAPrivateKey key) {
|
||||
final d = key.privateExponent!;
|
||||
final p = key.p!;
|
||||
final q = key.q!;
|
||||
final dModP = d % (p - BigInt.from(1));
|
||||
final dModQ = d % (q - BigInt.from(1));
|
||||
final coefficient = q.modInverse(p);
|
||||
return _asn1Sequence([
|
||||
_asnZero,
|
||||
ASN1Integer(key.modulus!),
|
||||
ASN1Integer(key.publicExponent!),
|
||||
ASN1Integer(d),
|
||||
ASN1Integer(p),
|
||||
ASN1Integer(q),
|
||||
ASN1Integer(dModP),
|
||||
ASN1Integer(dModQ),
|
||||
ASN1Integer(coefficient),
|
||||
]).encodedBytes;
|
||||
}
|
||||
|
||||
/// Encodes [key] as PKCS8 binary
|
||||
Uint8List encodeRsaPrivKey(RSAPrivateKey key) => _asn1Sequence([
|
||||
_asnZero,
|
||||
_asn1Sequence([_asnRsaOid, _asn1Null]),
|
||||
ASN1OctetString(encodeRsaPrivKeyPKCS1(key))
|
||||
]).encodedBytes;
|
||||
@@ -1,310 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:pointycastle/asymmetric/api.dart';
|
||||
import 'package:pointycastle/digests/sha256.dart';
|
||||
import 'buffer.dart';
|
||||
import 'crypto.dart';
|
||||
import 'parser.dart';
|
||||
import 'protocol.dart';
|
||||
import 'rsa_keys.dart';
|
||||
|
||||
abstract class Transport {
|
||||
Future<Uint8List> read(int n);
|
||||
Future<void> write(Uint8List data);
|
||||
Future<void> close();
|
||||
}
|
||||
|
||||
class SMPServer {
|
||||
final String host;
|
||||
final int? port;
|
||||
final Uint8List? keyHash;
|
||||
SMPServer(this.host, [this.port, this.keyHash]);
|
||||
}
|
||||
|
||||
class SessionKey {
|
||||
final AESKey aesKey;
|
||||
final Uint8List baseIV;
|
||||
int _counter = 0;
|
||||
SessionKey._(this.aesKey, this.baseIV);
|
||||
static SessionKey create() => SessionKey._(AESKey.random(), randomIV());
|
||||
Uint8List serialize() => concat(aesKey.bytes, baseIV);
|
||||
}
|
||||
|
||||
class ServerHeader {
|
||||
final int blockSize;
|
||||
final int keySize;
|
||||
ServerHeader(this.blockSize, this.keySize);
|
||||
}
|
||||
|
||||
class ServerHandshake {
|
||||
final RSAPublicKey publicKey;
|
||||
final int blockSize;
|
||||
ServerHandshake(this.publicKey, this.blockSize);
|
||||
}
|
||||
|
||||
typedef SMPVersion = List<int>;
|
||||
|
||||
SMPVersion _currentSMPVersion = const [0, 4, 1, 0];
|
||||
const int _serverHeaderSize = 8;
|
||||
const int _binaryRsaTransport = 0;
|
||||
const int _transportBlockSize = 4096;
|
||||
const int _maxTransportBlockSize = 65536;
|
||||
|
||||
class _Request {
|
||||
final Uint8List queueId;
|
||||
final Completer<BrokerResponse> completer = Completer();
|
||||
_Request(this.queueId);
|
||||
}
|
||||
|
||||
class Either<L, R> {
|
||||
final L? left;
|
||||
final R? right;
|
||||
Either.left(L this.left) : right = null;
|
||||
Either.right(R this.right) : left = null;
|
||||
}
|
||||
|
||||
enum ClientErrorType { SMPServerError, SMPResponseError, SMPUnexpectedResponse }
|
||||
|
||||
class BrokerResponse {
|
||||
final BrokerCommand? command;
|
||||
final ClientErrorType? errorType;
|
||||
final ERR? error;
|
||||
BrokerResponse(BrokerCommand this.command)
|
||||
: errorType = null,
|
||||
error = null;
|
||||
BrokerResponse.error(ClientErrorType this.errorType, this.error)
|
||||
: command = null;
|
||||
}
|
||||
|
||||
class ClientTransmission {
|
||||
final String corrId;
|
||||
final Uint8List queueId;
|
||||
final ClientCommand command;
|
||||
ClientTransmission(this.corrId, this.queueId, this.command);
|
||||
|
||||
Uint8List serialize() =>
|
||||
unwordsN([encodeAscii(corrId), encode64(queueId), command.serialize()]);
|
||||
}
|
||||
|
||||
class BrokerTransmission {
|
||||
final String corrId;
|
||||
final Uint8List queueId;
|
||||
final BrokerCommand? command;
|
||||
final ERR? error;
|
||||
BrokerTransmission(this.corrId, this.queueId, BrokerCommand this.command)
|
||||
: error = null;
|
||||
BrokerTransmission.error(this.corrId, this.queueId, ERR this.error)
|
||||
: command = null;
|
||||
}
|
||||
|
||||
final badBlock = BrokerTransmission.error('', empty, ERR(ErrorType.BLOCK));
|
||||
|
||||
class SMPTransportClient {
|
||||
final Transport _conn;
|
||||
final _sndKey = SessionKey.create();
|
||||
final _rcvKey = SessionKey.create();
|
||||
final int blockSize;
|
||||
int _corrId = 0;
|
||||
bool _messageStreamCreated = false;
|
||||
final Map<String, _Request> _sentCommands = {};
|
||||
SMPTransportClient._(this._conn, this.blockSize);
|
||||
|
||||
static Future<SMPTransportClient> connect(Transport conn,
|
||||
{Uint8List? keyHash, int? blockSize}) {
|
||||
return _clientHandshake(conn, keyHash, blockSize);
|
||||
}
|
||||
|
||||
Future<void> close() {
|
||||
return _conn.close();
|
||||
}
|
||||
|
||||
Future<BrokerResponse> sendSMPCommand(
|
||||
RSAPrivateKey? key, Uint8List queueId, ClientCommand cmd) async {
|
||||
final corrId = (++_corrId).toString();
|
||||
final t = ClientTransmission(corrId, queueId, cmd).serialize();
|
||||
final sig = key == null ? empty : encode64(signPSS(key, t));
|
||||
final data = unwordsN([sig, t, empty]);
|
||||
final r = _sentCommands[corrId] = _Request(queueId);
|
||||
await _writeEncrypted(data);
|
||||
return r.completer.future;
|
||||
}
|
||||
|
||||
Stream<BrokerTransmission> messageStream() {
|
||||
if (_messageStreamCreated) {
|
||||
throw Exception('message stream already created');
|
||||
Stream<Uint8List> blockStream(Transport t, int blockSize) async* {
|
||||
try {
|
||||
while (true) {
|
||||
yield await t.read(blockSize);
|
||||
}
|
||||
_messageStreamCreated = true;
|
||||
return _messageStream();
|
||||
}
|
||||
|
||||
Stream<BrokerTransmission> _messageStream() async* {
|
||||
try {
|
||||
while (true) {
|
||||
final block = await _readEncrypted();
|
||||
final t = _parseBrokerTransmission(block);
|
||||
if (t.corrId == '') {
|
||||
yield t;
|
||||
} else {
|
||||
final r = _sentCommands.remove(t.corrId);
|
||||
if (r == null) {
|
||||
yield t;
|
||||
} else {
|
||||
final cmd = t.command;
|
||||
r.completer.complete(r.queueId.equal(t.queueId)
|
||||
? cmd == null
|
||||
? BrokerResponse.error(
|
||||
ClientErrorType.SMPResponseError, t.error)
|
||||
: cmd is ERR
|
||||
? BrokerResponse.error(
|
||||
ClientErrorType.SMPServerError, cmd)
|
||||
: BrokerResponse(cmd)
|
||||
: BrokerResponse.error(
|
||||
ClientErrorType.SMPUnexpectedResponse, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static BrokerTransmission _parseBrokerTransmission(Uint8List s) {
|
||||
final p = Parser(s);
|
||||
p.space();
|
||||
final cId = p.word();
|
||||
p.space();
|
||||
final queueId = p.tryParse((p) => p.base64()) ?? empty;
|
||||
p.space();
|
||||
if (p.fail || cId == null) return badBlock;
|
||||
final corrId = decodeAscii(cId);
|
||||
final command = smpCommandP(p);
|
||||
if (command == null) {
|
||||
return BrokerTransmission.error(
|
||||
corrId, queueId, ERR.cmd(CmdErrorType.SYNTAX));
|
||||
}
|
||||
if (command is! BrokerCommand) {
|
||||
return BrokerTransmission.error(
|
||||
corrId, queueId, ERR.cmd(CmdErrorType.PROHIBITED));
|
||||
}
|
||||
final qErr = _tQueueError(queueId, command);
|
||||
if (qErr != null) {
|
||||
return BrokerTransmission.error(corrId, queueId, ERR.cmd(qErr));
|
||||
}
|
||||
return BrokerTransmission(corrId, queueId, command);
|
||||
}
|
||||
|
||||
static CmdErrorType? _tQueueError(Uint8List queueId, BrokerCommand cmd) {
|
||||
if (cmd is IDS || cmd is PONG) {
|
||||
if (queueId.isNotEmpty) return CmdErrorType.HAS_AUTH;
|
||||
} else if (cmd is! ERR && queueId.isEmpty) {
|
||||
return CmdErrorType.NO_QUEUE;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<SMPTransportClient> _clientHandshake(
|
||||
Transport conn, Uint8List? keyHash, int? blkSize) async {
|
||||
final srv = await _getHeaderAndPublicKey_1_2(conn, keyHash);
|
||||
final t = SMPTransportClient._(conn, blkSize ?? srv.blockSize);
|
||||
await t._sendEncryptedKeys_4(srv.publicKey);
|
||||
_checkVersion(await t._getWelcome_6());
|
||||
return t;
|
||||
}
|
||||
|
||||
static Future<ServerHandshake> _getHeaderAndPublicKey_1_2(
|
||||
Transport conn, Uint8List? keyHash) async {
|
||||
final srvHeader = parseServerHeader(await conn.read(_serverHeaderSize));
|
||||
final blkSize = srvHeader.blockSize;
|
||||
if (blkSize < _transportBlockSize || blkSize > _maxTransportBlockSize) {
|
||||
throw Exception('smp handshake header error: bad block size $blkSize');
|
||||
}
|
||||
final rawKey = await conn.read(srvHeader.keySize);
|
||||
if (keyHash != null) validateKeyHash_2(rawKey, keyHash);
|
||||
final serverKey = decodeRsaPubKey(rawKey);
|
||||
return ServerHandshake(serverKey, blkSize);
|
||||
}
|
||||
|
||||
static void validateKeyHash_2(Uint8List rawKey, Uint8List keyHash) {
|
||||
if (keyHash.equal(SHA256Digest().process(rawKey))) return;
|
||||
throw Exception('smp handshake error: bad key hash');
|
||||
}
|
||||
|
||||
static ServerHeader parseServerHeader(Uint8List a) {
|
||||
if (a.length != 8) {
|
||||
throw Exception('smp handshake error: bad header size ${a.length}');
|
||||
}
|
||||
final v = ByteData.sublistView(a);
|
||||
final blockSize = v.getUint32(0);
|
||||
final transportMode = v.getUint16(4);
|
||||
if (transportMode != _binaryRsaTransport) {
|
||||
throw Exception('smp handshake error: bad transport mode $transportMode');
|
||||
}
|
||||
final keySize = v.getUint16(6);
|
||||
return ServerHeader(blockSize, keySize);
|
||||
}
|
||||
|
||||
Future<void> _sendEncryptedKeys_4(RSAPublicKey serverKey) async {
|
||||
final header = encryptOAEP(serverKey, _clientHeader());
|
||||
await _conn.write(header);
|
||||
}
|
||||
|
||||
Uint8List _clientHeader() => concatN([
|
||||
encodeInt32(blockSize),
|
||||
encodeInt16(_binaryRsaTransport),
|
||||
_sndKey.serialize(),
|
||||
_rcvKey.serialize()
|
||||
]);
|
||||
|
||||
Future<SMPVersion> _getWelcome_6() async =>
|
||||
_parseSMPVersion(await _readEncrypted());
|
||||
|
||||
static SMPVersion _parseSMPVersion(Uint8List block) {
|
||||
final p = Parser(block);
|
||||
final SMPVersion version = [0, 0, 0, 0];
|
||||
void setVer(int i, int? v) {
|
||||
if (v == null || p.fail) {
|
||||
throw Exception('smp handshake error: bad version format');
|
||||
}
|
||||
version[i] = v;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final v = p.decimal();
|
||||
p.char(charDot);
|
||||
setVer(i, v);
|
||||
}
|
||||
final v = p.decimal();
|
||||
p.space();
|
||||
setVer(3, v);
|
||||
return version;
|
||||
}
|
||||
|
||||
static void _checkVersion(SMPVersion srvVersion) {
|
||||
final s0 = srvVersion[0];
|
||||
final c0 = _currentSMPVersion[0];
|
||||
if (s0 > c0 || (s0 == c0 && srvVersion[1] > _currentSMPVersion[1])) {
|
||||
throw Exception('smp handshake error: incompatible server version');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> _readEncrypted() async {
|
||||
final block = await _conn.read(blockSize);
|
||||
final iv = _nextIV(_rcvKey);
|
||||
return decryptAES(_rcvKey.aesKey, iv, block);
|
||||
}
|
||||
|
||||
Future<void> _writeEncrypted(Uint8List data) {
|
||||
final iv = _nextIV(_sndKey);
|
||||
final block = encryptAES(_sndKey.aesKey, iv, blockSize - 16, data);
|
||||
return _conn.write(block);
|
||||
}
|
||||
|
||||
static Uint8List _nextIV(SessionKey sk) {
|
||||
final c = encodeInt32(sk._counter++);
|
||||
final start = sk.baseIV.sublist(0, 4);
|
||||
final rest = sk.baseIV.sublist(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
start[i] ^= c[i];
|
||||
}
|
||||
return concat(start, rest);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,9 @@ publish_to: none
|
||||
environment:
|
||||
sdk: '>=2.14.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
asn1lib: ^1.0.2
|
||||
pointycastle:
|
||||
git:
|
||||
url: https://github.com/simplex-chat/pc-dart.git
|
||||
ref: nullsafety
|
||||
|
||||
# pointycastle: ^3.3.4
|
||||
# dependencies:
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^1.0.0
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:simplexmq/src/buffer.dart';
|
||||
import 'package:simplexmq/src/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('AES-GCM encryption with padding', () {
|
||||
test('encrypt and decrypt', () {
|
||||
final key = AESKey.random();
|
||||
final iv = pseudoRandomBytes(16);
|
||||
final data = encodeAscii('hello');
|
||||
final cipherText = encryptAES(key, iv, 32, data);
|
||||
expect(cipherText.length, 32 + 16);
|
||||
final decrypted = decryptAES(key, iv, cipherText);
|
||||
expect(decodeAscii(decrypted),
|
||||
'hello' + List.filled(32 - 'hello'.length, '#').join());
|
||||
});
|
||||
});
|
||||
|
||||
group('RSA-OAEP encryption', () {
|
||||
test('encrypt and decrypt', () {
|
||||
final keyPair = generateRSAkeyPair();
|
||||
final data = encodeAscii('hello there');
|
||||
final cipherText = encryptOAEP(keyPair.publicKey, data);
|
||||
expect(cipherText.length, 2048 ~/ 8);
|
||||
final decrypted = decryptOAEP(keyPair.privateKey, cipherText);
|
||||
expect(decodeAscii(decrypted), 'hello there');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:simplexmq/src/buffer.dart';
|
||||
import 'package:simplexmq/src/rsa_keys.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('RSA keys encoding', () {
|
||||
test('SPKI encode/decode RSA public key', () {
|
||||
final keyStr =
|
||||
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fdE2lndTnPi7QHS2OqP1YE6ZH6sGdf7Boji6jPgQVJB289aQAPZRSJlg6s+xHC52sa2isFiuZN2uFENNWznuZsOWXkHMthbo9Qkp7ZjOhomZURtsIsaRny9GTcaFOrd19rqbsrCRLyb3xtwbQjv/2HEGNZyP9YsGsZijTJaV0yQNEp/5Gt3jHebJ8mqLdBr/aDQBf3oSsmUDDvocGU4kL14GOuVYCKNlEUrFe1X1poSXLH0uu485GVfHB72XjKP/flS2rL91fguqMil1nkelL1K4WOyx1Z87LyyXT2Vh4GRLVHG/a9LyPpw7ovQlO5RIr6suODkXwbAUHq/8j5IDwIDAQAB';
|
||||
final key = decodeRsaPubKey(decode64(encodeAscii(keyStr))!);
|
||||
final encKey = decodeAscii(encode64(encodeRsaPubKey(key)));
|
||||
expect(encKey, keyStr);
|
||||
});
|
||||
|
||||
test('PKCS8 encode/decode RSA private key', () {
|
||||
final keyStr =
|
||||
'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDZ90TaWd1Oc+LtAdLY6o/VgTpkfqwZ1/sGiOLqM+BBUkHbz1pAA9lFImWDqz7EcLnaxraKwWK5k3a4UQ01bOe5mw5ZeQcy2Fuj1CSntmM6GiZlRG2wixpGfL0ZNxoU6t3X2upuysJEvJvfG3BtCO//YcQY1nI/1iwaxmKNMlpXTJA0Sn/ka3eMd5snyaot0Gv9oNAF/ehKyZQMO+hwZTiQvXgY65VgIo2URSsV7VfWmhJcsfS67jzkZV8cHvZeMo/9+VLasv3V+C6oyKXWeR6UvUrhY7LHVnzsvLJdPZWHgZEtUcb9r0vI+nDui9CU7lEivqy44ORfBsBQer/yPkgPAgMBAAECggEAezFPgB4EgB/tpUk/k4xXiTPF/iC+QskYvyPFJNv3JtRIFuWGO+Iw/esn9xhlnH+d+/IOIDSXCQ44ropY7dZEzlm97YIDOJCikuEHaqciRCedheT8Hikwy6Aa/NJw8lug0SyRDdeZn2H+s0X98BJ6Gxx1yhgCcOQq/2MbNnS8LNQ0yNNHu9Ds2K5Weiwhb9nrLLuMvrF/k1z0QNi5mCzDZK8iDMr87UZycmKKue13/xppI1pddJm4Ta13/OmZtYe2d5UgK9FrLStFkl7yqWnIcDueCOZvqo4nIfxPlPVolQ9B9RXL2tctkYRVy6FBkZIJkSk4O1Vz5BuPgBy9McoRMQKBgQD5xmVKAnvBQ08BUGXw5HVy4qR1Oj/EmxXKNypZsTdaKpiMfjNB8GbegO0na9Ry7sRo5g15vXpjCS6LDwGx4fZ/5u4N5CP3845DscrZjebs0tS7u5USPMoMzZ/KYfddRsGdm7y9HMp3Z6O3ZGGqZ15VFHGYjRjzK8BYE91rvv1WlwKBgQDfZe0c8/aa25KHW2zLRtSOR8ze+pz79hxNmpCeOoyzfaVRzwDh3KUl78PEWqMbJ7EdEdokCisFU0yEpuuc29JD6l9YuQmYH2VGdyg52iPPCXJOP1PBO0VQ/D/cAmZd/75cmoiyC7kELHfiAWBqO/7xpWkNiEpZZcI33DbzReYBSQKBgBKGuaqUppM+J9UEHpuQhnmf/+zGBkbR7frSvqxqbZ2dfTUmgyzH5Qlp7K043UgtF5pkPemiuToxSyd7VHfaN8ti2JNlMZnJkerJfC9IzDESrj7CehshMSdj9Q8w1wUvI1tKWuR4Bzh2Enme03OtORz8aDSVep1GyHx/9LNyNh4/AoGAcpdk/nIB8ENrMTVrZAYsJ+OaqlIhTnla4U/EmPVtkPCFaaZmTHUS3ZfUcpcPjXFZv5CVteDlWnD1EiJRP3/epmnFiMw5qKeKGpAquSo1LhEpagu/2aGel8EcvK0ad2Mk8XlvXuz2dbads/eCzluCFdAESCW+BYdWDbNPGJClP8kCgYBoT+0res0efi1cn6H0fPx/q33Wmgf47txVrzQN0ZEWDFOOhnErvGpRan9AG+LGvp7TvWWHnW13qjFCXGocWcbaoqsLabkov961R8ij2MTeToz6V+7YfK0KBt/h2HHJ5t/CybNxE5iYFyUMI7GTlC2GzFrnvxH/UYwUma1AplkpEw==';
|
||||
final key = decodeRsaPrivKey(decode64(encodeAscii(keyStr))!);
|
||||
final encKey = decodeAscii(encode64(encodeRsaPrivKey(key)));
|
||||
expect(encKey, keyStr);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
import 'dart:io';
|
||||
// import 'package:simplexmq_io/simplexmq_io.dart';
|
||||
|
||||
void main() {
|
||||
ServerSocket.bind('localhost', 8080)
|
||||
.then((server) => server.listen((Socket socket) {
|
||||
print('New client connection');
|
||||
socket.listen((List<int> data) {
|
||||
String result = String.fromCharCodes(data);
|
||||
print(result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
// void main() {
|
||||
// var awesome = Awesome();
|
||||
// print('awesome: ${awesome.isAwesome}');
|
||||
// }
|
||||
|
||||
@@ -20,22 +20,22 @@ class SocketTransport implements Transport {
|
||||
final int _bufferSize;
|
||||
Uint8List _buffer = Uint8List(0);
|
||||
final ListQueue<_STReaders> _readers = ListQueue(16);
|
||||
SocketTransport._(this._socket, this._timeout, this._bufferSize);
|
||||
SocketTransport._new(this._socket, this._timeout, this._bufferSize);
|
||||
|
||||
static Future<SocketTransport> connect(String host, int port,
|
||||
{Duration timeout = const Duration(seconds: 4),
|
||||
{Duration timeout = const Duration(seconds: 1),
|
||||
int bufferSize = 16384}) async {
|
||||
final socket = await Socket.connect(host, port, timeout: timeout);
|
||||
final t = SocketTransport._(socket, timeout, bufferSize);
|
||||
final t = SocketTransport._new(socket, timeout, bufferSize);
|
||||
// ignore: cancel_subscriptions
|
||||
final subscription = socket.listen(t._onData,
|
||||
onError: (Object e) => t.close, onDone: t.close);
|
||||
onError: (Object e) => t._finalize, onDone: t._finalize);
|
||||
t._subscription = subscription;
|
||||
return t;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> read(int n) {
|
||||
Future<Uint8List> read(int n) async {
|
||||
if (_readers.isEmpty && _buffer.length >= n) {
|
||||
final data = _buffer.sublist(0, n);
|
||||
_buffer = _buffer.sublist(n);
|
||||
@@ -71,14 +71,17 @@ class SocketTransport implements Transport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the client transport
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _subscription.cancel();
|
||||
void _finalize() {
|
||||
_subscription.cancel();
|
||||
_socket.destroy();
|
||||
while (_readers.isNotEmpty) {
|
||||
final r = _readers.removeFirst();
|
||||
r.completer.completeError(Exception('socket closed'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow closing the client transport.
|
||||
void close() {
|
||||
_finalize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies:
|
||||
git:
|
||||
url: git://github.com/simplex-chat/simplex-chat
|
||||
path: packages/simplexmq
|
||||
ref: ep/smp-client
|
||||
ref: ep/socket-transport
|
||||
|
||||
dependency_overrides:
|
||||
simplexmq:
|
||||
|
||||
@@ -12,7 +12,7 @@ void main() {
|
||||
group('transport', () {
|
||||
Future<ServerSocket> startServer(
|
||||
void Function(Socket client) handleConnection) async {
|
||||
var server = await ServerSocket.bind(localhost, 8080);
|
||||
var server = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
server.listen(handleConnection);
|
||||
return server;
|
||||
}
|
||||
@@ -27,11 +27,11 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
var transport = await SocketTransport.connect(localhost, 8080);
|
||||
var transport = await SocketTransport.connect(localhost, server.port);
|
||||
await transport.write(Uint8List.fromList([1, 2, 3]));
|
||||
|
||||
expect(await completer.future, [1, 2, 3]);
|
||||
await transport.close();
|
||||
transport.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ void main() {
|
||||
});
|
||||
var transport = await SocketTransport.connect(localhost, server.port);
|
||||
expect(await transport.read(3), [1, 2, 3]);
|
||||
await transport.close();
|
||||
transport.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:simplexmq/simplexmq.dart';
|
||||
import 'package:simplexmq/src/buffer.dart';
|
||||
import 'package:simplexmq/src/crypto.dart';
|
||||
import 'package:simplexmq/src/rsa_keys.dart';
|
||||
import 'package:simplexmq_io/simplexmq_io.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final keyHash =
|
||||
decode64(encodeAscii('pH7bg7B6vB3uJ1poKmClTAqr7yYWnAtapnIDN7ypKxU='));
|
||||
void main() {
|
||||
group('SMP transport (expects SMP server on localhost:5223)', () {
|
||||
test('establish connection', () async {
|
||||
final conn = await SocketTransport.connect('localhost', 5223);
|
||||
final smp = await SMPTransportClient.connect(conn, keyHash: keyHash);
|
||||
expect(smp is SMPTransportClient, true);
|
||||
});
|
||||
|
||||
test('should create SMP queue and send message', () async {
|
||||
final conn1 = await SocketTransport.connect('localhost', 5223);
|
||||
final alice = await SMPTransportClient.connect(conn1, keyHash: keyHash);
|
||||
final aliceMessages = alice.messageStream();
|
||||
final aliceKeys = generateRSAkeyPair();
|
||||
final rcvKeyBytes = encodeRsaPubKey(aliceKeys.publicKey);
|
||||
// input stream is not processed without the call to listen()
|
||||
aliceMessages.listen((_) {});
|
||||
|
||||
// final conn2 = await SocketTransport.connect('localhost', 5223);
|
||||
// final bob = await SMPTransportClient.connect(conn2, keyHash: keyHash);
|
||||
// final bobKeys = generateRSAkeyPair();
|
||||
// final sndKeyStr = encode64(encodeRsaPubKey(bobKeys.publicKey));
|
||||
|
||||
final resp = await alice.sendSMPCommand(
|
||||
aliceKeys.privateKey, empty, NEW(rcvKeyBytes));
|
||||
expect(resp.command is IDS, true);
|
||||
});
|
||||
// });
|
||||
}, skip: 'requires SMP server on port 5223');
|
||||
}
|
||||