Compare commits
71 Commits
test/max_o
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
693d2ad79e
|
|||
|
f19e8950e8
|
|||
|
64d0541dc6
|
|||
|
dfb4d9d9e5
|
|||
| 6306555a30 | |||
| b6b1743285 | |||
|
f6a48140b4
|
|||
|
cd79cf4449
|
|||
|
1aadd9c368
|
|||
|
febc0a8f43
|
|||
|
fd5e714074
|
|||
|
c108859899
|
|||
|
b3083d37c3
|
|||
|
521c1e94c0
|
|||
|
31a45bc4c3
|
|||
|
b6944d1dbb
|
|||
|
32ef2c5023
|
|||
|
95027c055c
|
|||
|
beff4d980b
|
|||
|
f39bfe6106
|
|||
|
bafcff7be4
|
|||
|
9862fda9e5
|
|||
|
038287ba4c
|
|||
|
3ae2742033
|
|||
|
faa624e9f8
|
|||
|
cc13f8a0f3
|
|||
|
56db0929d1
|
|||
|
658dc4cc9c
|
|||
|
3e0c4845e9
|
|||
|
255fc9337c
|
|||
|
7bbe321d3c
|
|||
|
9db49a4164
|
|||
|
1d2f4f70c8
|
|||
|
d1eecad059
|
|||
|
d5e9c6ecc3
|
|||
|
b91ddc172d
|
|||
|
5417796f3f
|
|||
|
78c895547e
|
|||
|
1f0f280286
|
|||
|
b280465914
|
|||
|
967ae915b2
|
|||
|
24cd1692c6
|
|||
|
63bc71c405
|
|||
|
a43a3b441f
|
|||
|
ab4b40ab75
|
|||
|
e9c5c5fb99
|
|||
|
b989a8359e
|
|||
|
6ec1d80f49
|
|||
|
c1e465020f
|
|||
|
b687464d59
|
|||
|
3239a075fb
|
|||
|
8c0f0e3e8f
|
|||
|
aac34ef738
|
|||
|
e96be86314
|
|||
|
95353735b0
|
|||
|
c0b8a8a3f4
|
|||
|
301240b896
|
|||
|
86b8c47ed5
|
|||
|
bc99f46720
|
|||
|
9c53cc52e9
|
|||
|
e6709cd4af
|
|||
|
cdb92757aa
|
|||
|
3c5da802a7
|
|||
|
05e2fcf185
|
|||
|
8ebd95a4b8
|
|||
|
80d4e18a23
|
|||
|
cc672d2f20
|
|||
|
1cf14e65a9
|
|||
|
9b2e429d3d
|
|||
|
2f73a21a41
|
|||
|
05eb37bc80
|
@@ -3,6 +3,12 @@
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
|
||||
# Configurable with a few commit messages:
|
||||
# - [skip-tests] Skip the test stage
|
||||
# - [skip-deployment] Skip the deployment stage
|
||||
# - [skip-ci] Skip all stages (the whole ci/cd)
|
||||
#
|
||||
|
||||
name: Build Docker and Deploy
|
||||
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
||||
|
||||
@@ -16,6 +22,9 @@ jobs:
|
||||
build_server:
|
||||
name: Build Docker Container
|
||||
runs-on: bfb-cicd-latest
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip-deployment]')
|
||||
steps:
|
||||
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
|
||||
- name: Check out code
|
||||
@@ -27,6 +36,9 @@ jobs:
|
||||
test_server:
|
||||
name: Run Unit-Tests
|
||||
runs-on: bfb-cicd-latest
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip-tests]')
|
||||
steps:
|
||||
|
||||
- name: Check out code
|
||||
@@ -68,6 +80,12 @@ jobs:
|
||||
name: Deploy to Server
|
||||
needs: [build_server, test_server]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
!cancelled() &&
|
||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip-deployment]') &&
|
||||
needs.build_server.result == 'success' &&
|
||||
(needs.test_server.result == 'skipped' || needs.test_server.result == 'success')
|
||||
steps:
|
||||
- name: Execute deploy on remote (via ssh)
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.aider*
|
||||
6
android/.idea/AndroidProjectSystem.xml
generated
Normal file
6
android/.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
263
android/.idea/other.xml
generated
263
android/.idea/other.xml
generated
@@ -1,263 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="direct_access_persist.xml">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q2q" />
|
||||
<option name="id" value="q2q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold3" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1768" />
|
||||
<option name="screenY" value="2208" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="x1q" />
|
||||
<option name="id" value="x1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S20" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3200" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
17
android/.idea/runConfigurations.xml
generated
Normal file
17
android/.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -30,6 +30,7 @@ android {
|
||||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
}
|
||||
namespace 'com.blackforestbytes.simplecloudnotifier'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.blackforestbytes.simplecloudnotifier">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
@@ -7,8 +7,8 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.google.gms:google-services:4.3.4'
|
||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,3 +14,6 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
||||
@@ -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.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
BIN
data/appicon_1.1.xcf
Normal file
BIN
data/appicon_1.1.xcf
Normal file
Binary file not shown.
6
flutter/.gitignore
vendored
6
flutter/.gitignore
vendored
@@ -5,6 +5,8 @@
|
||||
firepit-log.txt
|
||||
flutter_jank_*
|
||||
|
||||
_releases/*
|
||||
|
||||
|
||||
#######################################################################################################################
|
||||
|
||||
@@ -18,9 +20,11 @@ flutter_jank_*
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
@@ -54,3 +58,5 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
/lib/git_stamp/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "41456452f29d64e8deb623a3c927524bcf9f111b"
|
||||
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,26 +13,26 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: android
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: ios
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: linux
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: macos
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: web
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
- platform: windows
|
||||
create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b
|
||||
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
|
||||
|
||||
run:
|
||||
flutter pub run build_runner build
|
||||
flutter run
|
||||
HASH=$(shell git rev-parse HEAD)
|
||||
VERS=$(shell cat pubspec.yaml | grep -oP '(?<=version: ).*' | sed 's/[\s]*//' | tr -d '\n' | tr -d '') # lazy evaluated!
|
||||
|
||||
run-android:
|
||||
java:
|
||||
sudo archlinux-java set java-17-openjdk
|
||||
java -version
|
||||
|
||||
# runs app locally (linux)
|
||||
run-linux: java gen
|
||||
dart run build_runner build
|
||||
_JAVA_OPTIONS="" flutter run -d linux
|
||||
|
||||
# runs app locally (web | not really supported)
|
||||
run-web: java gen
|
||||
dart run build_runner build
|
||||
_JAVA_OPTIONS="" flutter run -d chrome
|
||||
|
||||
# runs on android device (must have network adb enabled teh correct IP)
|
||||
run-android: java gen
|
||||
ping -c1 10.10.10.177
|
||||
adb connect 10.10.10.177:5555
|
||||
flutter pub run build_runner build
|
||||
flutter run -d 10.10.10.177:5555
|
||||
dart run build_runner build
|
||||
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
|
||||
|
||||
install-release: java gen
|
||||
# Install on Pixel 7a
|
||||
flutter build apk --release
|
||||
flutter run --release -d 35221JEHN07157
|
||||
|
||||
release: java gen
|
||||
@_utils/release.sh
|
||||
|
||||
test:
|
||||
dart analyze
|
||||
@@ -16,12 +38,37 @@ test:
|
||||
fix:
|
||||
dart fix --apply
|
||||
|
||||
gen:
|
||||
flutter pub run build_runner build
|
||||
gen: java
|
||||
./_utils/inc_buildnum.sh
|
||||
dart run build_runner build
|
||||
dart run git_stamp git_stamp --build-type lite --limit 2
|
||||
|
||||
# run `make run` in another terminal (or another variant of flutter run)
|
||||
autoreload:
|
||||
@# run `make run` in another terminal (or another variant of flutter run)
|
||||
@_utils/autoreload.sh
|
||||
|
||||
icons:
|
||||
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
|
||||
|
||||
clean:
|
||||
flutter clean
|
||||
flutter pub get
|
||||
|
||||
clean-ios: clean
|
||||
cd ios && xcodebuild clean && rm -rf Pods Podfile.lock && pod install
|
||||
|
||||
clean-android: clean
|
||||
cd android && ./gradlew clean
|
||||
|
||||
|
||||
# upgrade all packages (add --major-versions even updates across new major versions)
|
||||
# https://docs.flutter.dev/release/upgrade
|
||||
# upgrading flutter can be done via `flutter upgrade`: https://docs.flutter.dev/release/upgrade
|
||||
# android/gradle updates should be done via androidStudio: https://docs.flutter.dev/release/breaking-changes/android-java-gradle-migration-guide
|
||||
upgrade:
|
||||
flutter upgrade
|
||||
flutter pub upgrade
|
||||
flutter doctor
|
||||
|
||||
aider:
|
||||
aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
|
||||
# TODO
|
||||
|
||||
- [ ] Message List
|
||||
* [ ] CRUD
|
||||
- [ ] Message Big-View
|
||||
- [ ] Search/Filter Messages
|
||||
- [ ] Channel List
|
||||
* [ ] Show subs
|
||||
* [ ] CRUD
|
||||
* [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
||||
- [ ] Sub List
|
||||
* [ ] Sub/Unsub/Accept/Deny
|
||||
- [ ] Debug List (Show logs, requests)
|
||||
- [ ] Key List
|
||||
* [ ] CRUD
|
||||
- [ ] Auto R-only key for admin, use for QR+link+send
|
||||
- [x] Message List
|
||||
* [x] CRUD
|
||||
- [x] Message Big-View
|
||||
- [x] Search/Filter Messages
|
||||
- [x] Channel List
|
||||
* [x] Show subs
|
||||
* [x] CRUD
|
||||
* [x] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
||||
- [x] Sub List
|
||||
* [x] Sub/Unsub/Accept/Deny
|
||||
- [x] Debug List (Show logs, requests)
|
||||
- [x] Key List
|
||||
* [x] CRUD
|
||||
- [x] Auto R-only key for admin, use for QR+link+send
|
||||
- [ ] settings
|
||||
- [ ] notifications
|
||||
- [ ] push navigation stack
|
||||
- [ ] read + migrate old SharedPrefs (or not? - who uses SCN even??)
|
||||
- [ ] Account-Page
|
||||
- [ ] Logout
|
||||
- [ ] Send-page
|
||||
- [?] notifications
|
||||
- [?] push navigation stack
|
||||
- [/] read + migrate old SharedPrefs (or not? - who uses SCN even??)
|
||||
- [x] Account-Page
|
||||
- [x] Logout
|
||||
- [x] Send-page
|
||||
|
||||
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
|
||||
- [x] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
|
||||
|
||||
- [x] fix time format (in message-list, in card, top right) - midnight is shown as "24:05" instead of "00:05" - thats weird
|
||||
|
||||
- [x] Add scrollbar
|
||||
-> https://api.flutter.dev/flutter/material/Scrollbar-class.html
|
||||
|
||||
- [x] you cant unsubscribe from foreign channel without completely loosing subscription.
|
||||
perhaps subscriptions should have two cofirmed bool (both must be true to receive messages): confirmed-owner && confirmed-subscriber
|
||||
Then the subscriber can unconfirm his half - without loosing the owner confirmation
|
||||
|
||||
-----
|
||||
|
||||
@@ -51,4 +60,4 @@
|
||||
- [ ] Disable compat | remove code
|
||||
- [x] compat message title
|
||||
- [ ] ...
|
||||
- [ ] RWLock directly in go - prevent/reduce db-locked exception
|
||||
- [x] RWLock directly in go - prevent/reduce db-locked exception
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
||||
|
||||
set -o nounset # disallow usage of unset vars ( set -u )
|
||||
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||
#set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||
#set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
||||
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
||||
|
||||
@@ -24,23 +24,34 @@
|
||||
|
||||
|
||||
|
||||
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
|
||||
pids="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )"
|
||||
|
||||
if [ -z "$pid" ]; then
|
||||
if [ -z "$pids" ]; then
|
||||
red "No [flutter run] process found - exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trap 'echo "reseived SIGNAL<EXIT> - exiting"; exit 0' EXIT
|
||||
trap 'echo "reseived SIGNAL<SIGINT> - exiting"; exit 0' SIGINT
|
||||
trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; exit 0' SIGTERM
|
||||
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; exit 0' SIGQUIT
|
||||
trap 'echo "reseived SIGNAL<EXIT> - exiting"; jobs -p | xargs kill ; exit 0' EXIT
|
||||
trap 'echo "reseived SIGNAL<SIGINT> - exiting"; jobs -p | xargs kill ; exit 0' SIGINT
|
||||
trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; jobs -p | xargs kill ; exit 0' SIGTERM
|
||||
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; jobs -p | xargs kill ; exit 0' SIGQUIT
|
||||
|
||||
echo ""
|
||||
while IFS= read -r pid; do
|
||||
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
||||
done <<< "$pids"
|
||||
|
||||
echo ""
|
||||
|
||||
while IFS= read -r pid; do
|
||||
{
|
||||
while true; do
|
||||
find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid";
|
||||
yellow 'File list changed - restart';
|
||||
done
|
||||
} &
|
||||
done <<< "$pids"
|
||||
|
||||
wait # wait for all background jobs to finish
|
||||
|
||||
echo "DONE."
|
||||
41
flutter/_utils/inc_buildnum.sh
Executable file
41
flutter/_utils/inc_buildnum.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
||||
|
||||
set -o nounset # disallow usage of unset vars ( set -u )
|
||||
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
||||
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
cr=$'\n'
|
||||
|
||||
function black() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[30m$1\\x1B[0m"; else echo "$1"; fi }
|
||||
function red() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[31m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function green() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[32m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function yellow(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[33m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function blue() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[34m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function purple(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[35m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function cyan() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[36m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
function white() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[37m$1\\x1B[0m"; else echo "$1"; fi; }
|
||||
|
||||
|
||||
|
||||
|
||||
path_to_pubspec="$(dirname "$0")/../pubspec.yaml"
|
||||
current_version=$(awk '/^version:/ {print $2}' $path_to_pubspec)
|
||||
current_version_without_build=$(echo "$current_version" | sed 's/\+.*//')
|
||||
|
||||
gitcount="$(git log | grep "^commit" | wc -l | xargs)"
|
||||
new_version="$current_version_without_build+$gitcount"
|
||||
|
||||
echo "Setting pubspec.yaml version $current_version to $new_version"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS sed (requires a space after -i)
|
||||
sed -i '' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec
|
||||
else
|
||||
# GNU sed (requires no space after -i)
|
||||
sed -i'' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec
|
||||
fi
|
||||
48
flutter/_utils/release.sh
Executable file
48
flutter/_utils/release.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -d ".git" ]]; then
|
||||
|
||||
echo "Must be called in project root"
|
||||
exit 1
|
||||
|
||||
fi
|
||||
|
||||
VERS="$(cat pubspec.yaml | grep -oP '(?<=version: ).*' | sed 's/[\s]*//' | tr -d '\n' | tr -d '')"
|
||||
|
||||
VERS_BY_SPEC="$( echo -n "$VERS" | awk -F'+' '{print "v"$1}' )"
|
||||
VERS_BY_TAG="$(git describe --abbrev=0 --tags)"
|
||||
|
||||
if [[ "$VERS_BY_TAG" != "$VERS_BY_SPEC" ]]; then
|
||||
echo "Version in pubspec.yaml ($VERS_BY_SPEC) does not match latest git tag ($VERS_BY_TAG)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "(!) Make sure you've updated version-number in pubspec.yaml (current = ${VERS}) !"
|
||||
echo 'Confirmed' && read -r
|
||||
echo ""
|
||||
|
||||
flutter build apk --release
|
||||
cp build/app/outputs/flutter-apk/app-release.apk "_releases/v${VERS}.apk"
|
||||
|
||||
echo ""
|
||||
echo "--> copied APK to _releases ( Version: ${VERS} )"
|
||||
echo ""
|
||||
|
||||
flutter build appbundle --release
|
||||
cp build/app/outputs/bundle/release/app-release.aab "_releases/v${VERS}.aab"
|
||||
cd "build/app/intermediates/merged_native_libs/release/out/lib" && zip -r "../../../../../../../_releases/v${VERS}.symbols.zip" .
|
||||
|
||||
echo ""
|
||||
echo "--> copied AAB to _releases ( Version: ${VERS} )"
|
||||
echo ""
|
||||
|
||||
flutter build linux --release
|
||||
tar -czf "_releases/v${VERS}.tar.gz" -C build/linux/x64/release/bundle .
|
||||
|
||||
echo ""
|
||||
echo "--> copied linux-binary to _releases ( Version: ${VERS} )"
|
||||
echo ""
|
||||
echo "#=> file://$(pwd)/_releases"
|
||||
echo ""
|
||||
echo "Done."
|
||||
4
flutter/android/.gitignore
vendored
4
flutter/android/.gitignore
vendored
@@ -11,3 +11,7 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
||||
build/
|
||||
|
||||
app/.cxx/
|
||||
@@ -38,12 +38,12 @@ android {
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
|
||||
@@ -23,7 +23,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "com.android.application" version "8.9.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
// END: FlutterFire Configuration
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
483C144A8FD9FBE0BE4CA31D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
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 */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
F0CF5BB613220E8F87B9D978 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -40,12 +42,17 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
12F3806130EB6FDA0BB8224F /* 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>"; };
|
||||
20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
7CD878882EBBB898002F3C7D /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; 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; };
|
||||
@@ -53,8 +60,12 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A4D58F2C21BD1C00F0A1FE7D /* 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>"; };
|
||||
AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EE5AE3E3797C8A45B5AFA537 /* 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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -62,12 +73,51 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F0CF5BB613220E8F87B9D978 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
985FE5E285D4547AB0054913 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
483C144A8FD9FBE0BE4CA31D /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
04ACC4FC139085193359BE10 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A4D58F2C21BD1C00F0A1FE7D /* Pods-Runner.debug.xcconfig */,
|
||||
EE5AE3E3797C8A45B5AFA537 /* Pods-Runner.release.xcconfig */,
|
||||
12F3806130EB6FDA0BB8224F /* Pods-Runner.profile.xcconfig */,
|
||||
DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */,
|
||||
AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */,
|
||||
D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
51BA414B0BC240BA4DED076E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDC5CDC3E1AFF7C5BC49C07E /* Pods_Runner.framework */,
|
||||
20818F4FAA848E7B0E1981A1 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -79,14 +129,6 @@
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -94,6 +136,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
04ACC4FC139085193359BE10 /* Pods */,
|
||||
51BA414B0BC240BA4DED076E /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -109,6 +153,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7CD878882EBBB898002F3C7D /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@@ -128,9 +173,10 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
827CC5F079B73B9074C059BC /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807E294A63A400263BE5 /* Frameworks */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
985FE5E285D4547AB0054913 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -146,12 +192,15 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
1A8E37578D7C990159141841 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
81B31B4B9605351C20E486B9 /* [CP] Embed Pods Frameworks */,
|
||||
14FC8825887E7053BC1CD5F8 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -169,7 +218,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
@@ -223,6 +272,45 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
14FC8825887E7053BC1CD5F8 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
1A8E37578D7C990159141841 /* [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;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -239,6 +327,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
81B31B4B9605351C20E486B9 /* [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;
|
||||
};
|
||||
827CC5F079B73B9074C059BC /* [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-RunnerTests-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;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -308,6 +435,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -337,6 +465,7 @@
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -345,7 +474,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -356,19 +485,26 @@
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
baseConfigurationReference = 12F3806130EB6FDA0BB8224F /* Pods-Runner.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = H5FLAAV8F2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -377,7 +513,7 @@
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */;
|
||||
baseConfigurationReference = DF96ECE6A0AC0BFA8724174B /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -395,7 +531,7 @@
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */;
|
||||
baseConfigurationReference = AE13A81982696D2690FB1CAB /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -411,7 +547,7 @@
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */;
|
||||
baseConfigurationReference = D208168B6EAB5683BAD27E86 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -427,8 +563,10 @@
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -458,6 +596,7 @@
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -472,7 +611,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -482,8 +621,10 @@
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -513,6 +654,7 @@
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -521,7 +663,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -534,19 +676,26 @@
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
baseConfigurationReference = A4D58F2C21BD1C00F0A1FE7D /* Pods-Runner.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = H5FLAAV8F2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -556,19 +705,26 @@
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
baseConfigurationReference = EE5AE3E3797C8A45B5AFA537 /* Pods-Runner.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = H5FLAAV8F2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SimpleCloudNotifier;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.simplecloudnotifier;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.blackforestbytes.SimpleCloudNotifier;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -26,6 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -54,11 +55,13 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,7 +2,7 @@ import UIKit
|
||||
import Flutter
|
||||
import flutter_local_notifications
|
||||
|
||||
@UIApplicationMain
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
|
||||
override func application(
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -24,6 +26,17 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs photos access to get QR code from photo library</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,9 +54,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -5,6 +5,8 @@ import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/models/api_error.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/send_message_response.dart';
|
||||
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
@@ -30,26 +32,45 @@ enum ChannelSelector {
|
||||
class MessageFilter {
|
||||
List<String>? channelIDs;
|
||||
List<String>? searchFilter;
|
||||
List<String>? plainSearchFilter;
|
||||
List<String>? senderNames;
|
||||
List<String>? usedKeys;
|
||||
List<int>? priority;
|
||||
DateTime? timeBefore;
|
||||
DateTime? timeAfter;
|
||||
bool? hasSenderName;
|
||||
List<String>? senderUserID;
|
||||
|
||||
MessageFilter({
|
||||
this.channelIDs,
|
||||
this.searchFilter,
|
||||
this.plainSearchFilter,
|
||||
this.senderNames,
|
||||
this.usedKeys,
|
||||
this.priority,
|
||||
this.timeBefore,
|
||||
this.timeAfter,
|
||||
this.senderUserID,
|
||||
});
|
||||
}
|
||||
|
||||
class SubscriptionFilter {
|
||||
static final SubscriptionFilter ALL = SubscriptionFilter('both', 'all', 'all');
|
||||
static final SubscriptionFilter OWNED_INACTIVE = SubscriptionFilter('outgoing', 'unconfirmed', 'false');
|
||||
static final SubscriptionFilter OWNED_ACTIVE = SubscriptionFilter('outgoing', 'confirmed', 'false');
|
||||
static final SubscriptionFilter EXTERNAL_ALL = SubscriptionFilter('outgoing', 'all', 'true');
|
||||
static final SubscriptionFilter INCOMING_ALL = SubscriptionFilter('incoming', 'all', 'true');
|
||||
|
||||
final String direction; // 'outgoing' | 'incoming' | 'both'
|
||||
final String confirmation; // 'confirmed' | 'unconfirmed' | 'all'
|
||||
final String external; // 'true' | 'false' | 'all'
|
||||
|
||||
SubscriptionFilter(this.direction, this.confirmation, this.external) {}
|
||||
}
|
||||
|
||||
class APIClient {
|
||||
static const String _base = 'https://simplecloudnotifier.de/api/v2';
|
||||
static const String _base = 'https://simplecloudnotifier.de';
|
||||
static const String _prefix = '/api/v2';
|
||||
|
||||
static Future<T> _request<T>({
|
||||
required String name,
|
||||
@@ -60,10 +81,11 @@ class APIClient {
|
||||
dynamic jsonBody,
|
||||
String? authToken,
|
||||
Map<String, String>? header,
|
||||
bool? nonAPI,
|
||||
}) async {
|
||||
final t0 = DateTime.now();
|
||||
|
||||
final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {});
|
||||
final uri = Uri.parse('$_base${(nonAPI ?? false) ? '' : _prefix}/$relURL').replace(queryParameters: query ?? {});
|
||||
|
||||
final req = http.Request(method, uri);
|
||||
|
||||
@@ -101,21 +123,23 @@ class APIClient {
|
||||
}
|
||||
|
||||
if (responseStatusCode != 200) {
|
||||
try {
|
||||
final apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
|
||||
APIError apierr;
|
||||
|
||||
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
|
||||
Toaster.error("Error", 'Request "${name}" failed');
|
||||
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
|
||||
try {
|
||||
apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace);
|
||||
}
|
||||
|
||||
RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
|
||||
Toaster.error("Error", 'Request "${name}" failed');
|
||||
throw Exception('API request failed with status code ${responseStatusCode}');
|
||||
}
|
||||
|
||||
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
|
||||
Toaster.error("Error", apierr.message);
|
||||
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message, true);
|
||||
}
|
||||
|
||||
try {
|
||||
final data = jsonDecode(responseBody);
|
||||
|
||||
@@ -159,6 +183,33 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<User> updateUser(TokenSource auth, String uid, {String? username, String? proToken}) async {
|
||||
return await _request(
|
||||
name: 'updateUser',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/$uid',
|
||||
jsonBody: {
|
||||
if (username != null) 'username': username,
|
||||
if (proToken != null) 'pro_token': proToken,
|
||||
},
|
||||
fn: User.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<User> deleteUser(TokenSource auth, String uid) async {
|
||||
return await _request(
|
||||
name: 'deleteUser',
|
||||
method: 'DELETE',
|
||||
relURL: 'users/$uid',
|
||||
fn: User.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
query: {
|
||||
'confirm': ['true']
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
|
||||
return await _request(
|
||||
name: 'addClient',
|
||||
@@ -176,16 +227,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Client> updateClient(TokenSource auth, String clientID, String fcmToken, String agentModel, String? name, String agentVersion) async {
|
||||
static Future<Client> updateClient(TokenSource auth, String clientID, {String? fcmToken, String? agentModel, String? name, String? agentVersion}) async {
|
||||
return await _request(
|
||||
name: 'updateClient',
|
||||
method: 'PUT',
|
||||
relURL: 'users/${auth.getUserID()}/clients/$clientID',
|
||||
jsonBody: {
|
||||
'fcm_token': fcmToken,
|
||||
'agent_model': agentModel,
|
||||
'agent_version': agentVersion,
|
||||
'name': name,
|
||||
if (fcmToken != null) 'fcm_token': fcmToken,
|
||||
if (agentModel != null) 'agent_model': agentModel,
|
||||
if (agentVersion != null) 'agent_version': agentVersion,
|
||||
if (name != null) 'name': name,
|
||||
},
|
||||
fn: Client.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
@@ -249,7 +300,7 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter}) async {
|
||||
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter, bool? includeNonSuscribed}) async {
|
||||
return await _request(
|
||||
name: 'getMessageList',
|
||||
method: 'GET',
|
||||
@@ -258,13 +309,16 @@ class APIClient {
|
||||
'next_page_token': [pageToken],
|
||||
if (pageSize != null) 'page_size': [pageSize.toString()],
|
||||
if (filter?.searchFilter != null) 'search': filter!.searchFilter!,
|
||||
if (filter?.plainSearchFilter != null) 'string_search': filter!.plainSearchFilter!,
|
||||
if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!,
|
||||
if (filter?.senderNames != null) 'sender': filter!.senderNames!,
|
||||
if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()],
|
||||
if (filter?.timeBefore != null) 'before': [filter!.timeBefore!.toIso8601String()],
|
||||
if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()],
|
||||
if (filter?.timeBefore != null) 'before': [filter!.timeBefore!.toUtc().toIso8601String()],
|
||||
if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toUtc().toIso8601String()],
|
||||
if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(),
|
||||
if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!,
|
||||
if (filter?.senderUserID != null) 'sender_user_id': filter!.senderUserID!,
|
||||
if (includeNonSuscribed ?? false) 'subscription_status': ['all'],
|
||||
},
|
||||
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
|
||||
authToken: auth.getToken(),
|
||||
@@ -281,11 +335,40 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
|
||||
static Future<(String, List<SCNMessage>)> getChannelMessageList(TokenSource auth, String cid, String pageToken, {int? pageSize}) async {
|
||||
return await _request(
|
||||
name: 'getChannelMessageList',
|
||||
method: 'GET',
|
||||
relURL: 'users/${auth.getUserID()}/channels/${cid}/messages',
|
||||
query: {
|
||||
'next_page_token': [pageToken],
|
||||
if (pageSize != null) 'page_size': [pageSize.toString()],
|
||||
},
|
||||
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> getSubscription(TokenSource auth, String subscriptionID) async {
|
||||
return await _request(
|
||||
name: 'getSubscription',
|
||||
method: 'GET',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subscriptionID}',
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<Subscription>> getSubscriptionList(TokenSource auth, SubscriptionFilter filter) async {
|
||||
return await _request(
|
||||
name: 'getSubscriptionList',
|
||||
method: 'GET',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||
query: {
|
||||
'direction': [filter.direction],
|
||||
'confirmation': [filter.confirmation],
|
||||
'external': [filter.external],
|
||||
},
|
||||
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
@@ -349,9 +432,9 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyTokenPreview> getKeyTokenPreview(TokenSource auth, String kid) async {
|
||||
static Future<KeyTokenPreview> getKeyTokenPreviewByID(TokenSource auth, String kid) async {
|
||||
return await _request(
|
||||
name: 'getKeyTokenPreview',
|
||||
name: 'getKeyTokenPreviewByID',
|
||||
method: 'GET',
|
||||
relURL: 'preview/keys/$kid',
|
||||
fn: KeyTokenPreview.fromJson,
|
||||
@@ -359,6 +442,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyTokenPreview> getKeyTokenPreviewByToken(TokenSource auth, String tok) async {
|
||||
return await _request(
|
||||
name: 'getKeyTokenPreviewByToken',
|
||||
method: 'GET',
|
||||
relURL: 'preview/keys/$tok',
|
||||
fn: KeyTokenPreview.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
||||
return await _request(
|
||||
name: 'getCurrentKeyToken',
|
||||
@@ -369,7 +462,155 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<String>> getSenderNameList(AppAuth userAcc) {
|
||||
return Future.value(['TODO']); //TODO
|
||||
static Future<void> deleteKeyToken(AppAuth acc, String keytokenID) {
|
||||
return _request(
|
||||
name: 'deleteKeyToken',
|
||||
method: 'DELETE',
|
||||
relURL: 'users/${acc.getUserID()}/keys/${keytokenID}',
|
||||
fn: (_) => null,
|
||||
authToken: acc.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
|
||||
return await _request(
|
||||
name: 'updateKeyToken',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/${auth.getUserID()}/keys/${kid}',
|
||||
jsonBody: {
|
||||
if (name != null) 'name': name,
|
||||
if (allChannels != null) 'all_channels': allChannels,
|
||||
if (channels != null) 'channels': channels,
|
||||
if (permissions != null) 'permissions': permissions,
|
||||
},
|
||||
fn: KeyToken.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyTokenWithToken> createKeyToken(TokenSource auth, String name, String perm, bool allChannels, {List<String>? channels}) async {
|
||||
return await _request(
|
||||
name: 'createKeyToken',
|
||||
method: 'POST',
|
||||
relURL: 'users/${auth.getUserID()}/keys',
|
||||
jsonBody: {
|
||||
'name': name,
|
||||
'permissions': perm,
|
||||
'all_channels': allChannels,
|
||||
if (channels != null) 'channels': channels,
|
||||
},
|
||||
fn: KeyTokenWithToken.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
|
||||
return await _request(
|
||||
name: 'getSenderNameList',
|
||||
method: 'GET',
|
||||
relURL: 'users/${auth.getUserID()}/sender-names',
|
||||
fn: (json) => SenderNameStatistics.fromJsonArray(json['sender_names'] as List<dynamic>),
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID, {String? subscribeKey}) async {
|
||||
return await _request(
|
||||
name: 'subscribeToChannelbyID',
|
||||
method: 'POST',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||
query: {
|
||||
if (subscribeKey != null) 'chan_subscribe_key': [subscribeKey],
|
||||
},
|
||||
jsonBody: {
|
||||
'channel_id': channelID,
|
||||
},
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> deleteSubscription(TokenSource auth, String channelID, String subID) async {
|
||||
return await _request(
|
||||
name: 'deleteSubscription',
|
||||
method: 'DELETE',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> confirmSubscription(TokenSource auth, String channelID, String subID) async {
|
||||
return await _request(
|
||||
name: 'confirmSubscription',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
||||
jsonBody: {
|
||||
'confirmed': true,
|
||||
},
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> unconfirmSubscription(TokenSource auth, String channelID, String subID) async {
|
||||
return await _request(
|
||||
name: 'unconfirmSubscription',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
||||
jsonBody: {
|
||||
'confirmed': false,
|
||||
},
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> activateSubscription(TokenSource auth, String channelID, String subID) async {
|
||||
return await _request(
|
||||
name: 'activateSubscription',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
||||
jsonBody: {
|
||||
'active': true,
|
||||
},
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Subscription> deactivateSubscription(TokenSource auth, String channelID, String subID) async {
|
||||
return await _request(
|
||||
name: 'deactivateSubscription',
|
||||
method: 'PATCH',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
||||
jsonBody: {
|
||||
'active': false,
|
||||
},
|
||||
fn: Subscription.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<SendMessageResponse> sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async {
|
||||
return await _request(
|
||||
name: 'sendMessage',
|
||||
method: 'POST',
|
||||
relURL: '/send',
|
||||
nonAPI: true,
|
||||
jsonBody: {
|
||||
'user_id': userid,
|
||||
'key': keytoken,
|
||||
'title': text,
|
||||
if (channel != null) 'channel': channel,
|
||||
if (content != null) 'content': content,
|
||||
if (priority != null) 'priority': priority,
|
||||
if (messageID != null) 'msg_id': messageID,
|
||||
if (timestamp != null) 'timestamp': (timestamp.microsecondsSinceEpoch / 1000).toInt(),
|
||||
if (senderName != null) 'sender_name': senderName,
|
||||
},
|
||||
fn: SendMessageResponse.fromJson,
|
||||
authToken: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
class APIException implements Exception {
|
||||
final int httpStatus;
|
||||
final int error;
|
||||
final String errHighlight;
|
||||
final int errHighlight;
|
||||
final String message;
|
||||
final bool toastShown;
|
||||
|
||||
APIException(this.httpStatus, this.error, this.errHighlight, this.message);
|
||||
APIException(this.httpStatus, this.error, this.errHighlight, this.message, this.toastShown);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
|
||||
105
flutter/lib/components/badge_display/badge_display.dart
Normal file
105
flutter/lib/components/badge_display/badge_display.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum BadgeMode { error, warn, info }
|
||||
|
||||
class BadgeDisplay extends StatefulWidget {
|
||||
final String text;
|
||||
final BadgeMode mode;
|
||||
final IconData? icon;
|
||||
final TextAlign textAlign;
|
||||
final bool closable;
|
||||
final EdgeInsets extraPadding;
|
||||
final bool hidden;
|
||||
|
||||
const BadgeDisplay({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.mode,
|
||||
required this.icon,
|
||||
this.textAlign = TextAlign.center,
|
||||
this.closable = false,
|
||||
this.extraPadding = EdgeInsets.zero,
|
||||
this.hidden = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BadgeDisplay> createState() => _BadgeDisplayState();
|
||||
}
|
||||
|
||||
class _BadgeDisplayState extends State<BadgeDisplay> {
|
||||
bool _isVisible = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isVisible || widget.hidden) return const SizedBox.shrink();
|
||||
|
||||
var col = Colors.grey;
|
||||
var colFG = Colors.black;
|
||||
|
||||
if (widget.mode == BadgeMode.error) col = Colors.red;
|
||||
if (widget.mode == BadgeMode.warn) col = Colors.orange;
|
||||
if (widget.mode == BadgeMode.info) col = Colors.blue;
|
||||
|
||||
if (widget.mode == BadgeMode.error) colFG = Colors.red[900]!;
|
||||
if (widget.mode == BadgeMode.warn) colFG = Colors.black;
|
||||
if (widget.mode == BadgeMode.info) colFG = Colors.black;
|
||||
|
||||
return Container(
|
||||
margin: widget.extraPadding,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// The badge itself
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(8, 2, widget.closable ? 16 : 8, 2),
|
||||
decoration: BoxDecoration(
|
||||
color: col[100],
|
||||
border: Border.all(color: col[300]!),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.icon != null) Icon(widget.icon!, color: colFG, size: 16.0),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.text,
|
||||
textAlign: widget.textAlign,
|
||||
style: TextStyle(color: colFG, fontSize: 14.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (widget.closable) _buildCloseButton(context, colFG),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Positioned _buildCloseButton(BuildContext context, Color colFG) {
|
||||
return Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isVisible = false;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 14,
|
||||
color: colFG,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
flutter/lib/components/error_display/error_display.dart
Normal file
38
flutter/lib/components/error_display/error_display.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class ErrorDisplay extends StatelessWidget {
|
||||
final String errorMessage;
|
||||
|
||||
const ErrorDisplay({Key? key, required this.errorMessage}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[100],
|
||||
border: Border.all(color: Colors.red[300]!),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.triangleExclamation, color: Colors.red, size: 48.0),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.red[900], fontSize: 16.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
flutter/lib/components/filter_chips/filter_chips.dart
Normal file
47
flutter/lib/components/filter_chips/filter_chips.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterChips<T extends Enum> extends StatelessWidget {
|
||||
final List<(T, String)> options;
|
||||
final T value;
|
||||
final void Function(T)? onChanged;
|
||||
|
||||
const FilterChips({
|
||||
Key? key,
|
||||
required this.options,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (var opt in options) _buildChiplet(context, opt.$1, opt.$2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChiplet(BuildContext context, T optValue, String optText) {
|
||||
final isSelected = optValue == value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 2, 4, 2),
|
||||
child: InputChip(
|
||||
label: Text(
|
||||
optText,
|
||||
style: isSelected ? TextStyle(color: Theme.of(context).colorScheme.onPrimary) : null,
|
||||
),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
isEnabled: true,
|
||||
selected: true,
|
||||
showCheckmark: false,
|
||||
onPressed: () {
|
||||
if (!isSelected) onChanged?.call(optValue);
|
||||
},
|
||||
selectedColor: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import 'package:simplecloudnotifier/components/layout/app_bar_filter_dialog.dart
|
||||
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||
@@ -64,7 +64,7 @@ class _SCNAppBarState extends State<SCNAppBar> {
|
||||
builder: (context, appTheme, child) => IconButton(
|
||||
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
|
||||
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
|
||||
onPressed: appTheme.switchDarkMode,
|
||||
onPressed: AppTheme().switchDarkMode,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
|
||||
@@ -3,9 +3,12 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_channel.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_keytoken.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_priority.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_searchplain.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_sendername.dart';
|
||||
import 'package:simplecloudnotifier/components/modals/filter_modal_time.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class AppBarFilterDialog extends StatefulWidget {
|
||||
@@ -16,7 +19,9 @@ class AppBarFilterDialog extends StatefulWidget {
|
||||
class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
|
||||
double _height = 0;
|
||||
|
||||
double _targetHeight = 4 + (48 * 6) + (16 * 5) + 4;
|
||||
static const int _itemCount = 7;
|
||||
|
||||
static const double _targetHeight = 4 + (48 * _itemCount) + (16 * (_itemCount - 1)) + 4;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -113,10 +118,24 @@ class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
|
||||
}
|
||||
|
||||
void _showTimeModal(BuildContext context) {
|
||||
showDialog<void>(context: context, builder: (BuildContext context) => FilterModalTime());
|
||||
final dateFormat = AppSettings().dateFormat.dateOnlyFormat();
|
||||
|
||||
final now = DateTime.now();
|
||||
showDateRangePicker(context: context, firstDate: DateTime(2000), lastDate: DateTime(now.year, now.month, now.day + 7)).then((value) {
|
||||
if (value != null) {
|
||||
List<MessageFilterChiplet> chiplets = [];
|
||||
chiplets.add(MessageFilterChiplet(
|
||||
label: dateFormat.format(value.start) + ' - ' + dateFormat.format(value.end),
|
||||
value: value,
|
||||
type: MessageFilterChipletType.timeRange,
|
||||
));
|
||||
|
||||
AppEvents().notifyFilterListeners([MessageFilterChipletType.plainSearch], chiplets);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showPlainSearchModal(BuildContext context) {
|
||||
//TODO showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
|
||||
showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:simplecloudnotifier/pages/send/send.dart';
|
||||
import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart';
|
||||
import 'package:simplecloudnotifier/pages/account/account.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_list.dart';
|
||||
import 'package:simplecloudnotifier/pages/settings/root.dart';
|
||||
import 'package:simplecloudnotifier/pages/settings/settings_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
@@ -20,20 +20,30 @@ class SCNNavLayout extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
int _selectedIndex = 0; // 4 == FAB
|
||||
static const INDEX_MESSAGES = 0;
|
||||
static const INDEX_CHANNELS = 1;
|
||||
static const INDEX_ACCOUNT = 2;
|
||||
static const INDEX_CONFIG = 3;
|
||||
static const INDEX_SEND = 4; // FAB
|
||||
|
||||
int _selectedIndex = INDEX_MESSAGES;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) _selectedIndex = 2;
|
||||
if (!userAcc.isAuth()) _selectedIndex = INDEX_ACCOUNT;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) {
|
||||
|
||||
if (!userAcc.isAuth() && [INDEX_MESSAGES, INDEX_CHANNELS, INDEX_SEND].contains(index)) {
|
||||
Toaster.info("Not logged in", "Please login or create a new account first");
|
||||
setState(() {
|
||||
_selectedIndex = INDEX_ACCOUNT;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +60,7 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_selectedIndex = 4;
|
||||
_selectedIndex = INDEX_SEND;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,17 +69,17 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
return Scaffold(
|
||||
appBar: SCNAppBar(
|
||||
title: null,
|
||||
showSearch: _selectedIndex == 0,
|
||||
showSearch: _selectedIndex == INDEX_MESSAGES,
|
||||
showShare: false,
|
||||
showThemeSwitch: true,
|
||||
),
|
||||
body: IndexedStack(
|
||||
children: [
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage(isVisiblePage: _selectedIndex == 0)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage(isVisiblePage: _selectedIndex == 1)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage(isVisiblePage: _selectedIndex == 2)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage(isVisiblePage: _selectedIndex == 3)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage(isVisiblePage: _selectedIndex == 4)),
|
||||
ExcludeFocus(excluding: _selectedIndex != INDEX_MESSAGES, child: MessageListPage(isVisiblePage: _selectedIndex == INDEX_MESSAGES)),
|
||||
ExcludeFocus(excluding: _selectedIndex != INDEX_CHANNELS, child: ChannelRootPage(isVisiblePage: _selectedIndex == INDEX_CHANNELS)),
|
||||
ExcludeFocus(excluding: _selectedIndex != INDEX_ACCOUNT, child: AccountRootPage(isVisiblePage: _selectedIndex == INDEX_ACCOUNT)),
|
||||
ExcludeFocus(excluding: _selectedIndex != INDEX_CONFIG, child: SettingsRootPage(isVisiblePage: _selectedIndex == INDEX_CONFIG)),
|
||||
ExcludeFocus(excluding: _selectedIndex != INDEX_SEND, child: SendRootPage(isVisiblePage: _selectedIndex == INDEX_SEND)),
|
||||
],
|
||||
index: _selectedIndex,
|
||||
),
|
||||
@@ -10,9 +10,11 @@ class SCNScaffold extends StatelessWidget {
|
||||
this.showSearch = true,
|
||||
this.showShare = false,
|
||||
this.onShare = null,
|
||||
this.floatingActionButton = null,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Widget? floatingActionButton;
|
||||
final String? title;
|
||||
final bool showThemeSwitch;
|
||||
final bool showSearch;
|
||||
@@ -30,6 +32,7 @@ class SCNScaffold extends StatelessWidget {
|
||||
onShare: onShare ?? () {},
|
||||
),
|
||||
body: child,
|
||||
floatingActionButton: floatingActionButton,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class FilterModalChannel extends StatefulWidget {
|
||||
@override
|
||||
@@ -15,13 +17,12 @@ class FilterModalChannel extends StatefulWidget {
|
||||
class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<Channel>>? _futureChannels;
|
||||
ImmediateFuture<List<Channel>> _futureChannels = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureChannels = null;
|
||||
_futureChannels = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
@@ -49,45 +50,39 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 9000,
|
||||
child: () {
|
||||
if (_futureChannels == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
future: _futureChannels!.future,
|
||||
child: FutureBuilder(
|
||||
future: _futureChannels.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (_futureChannels?.value != null) {
|
||||
return _buildList(context, _futureChannels!.value!);
|
||||
if (_futureChannels.value != null) {
|
||||
return _buildList(context, _futureChannels.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||
return _buildList(context, snapshot.data!);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||
}
|
||||
}),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Apply'),
|
||||
onPressed: () {
|
||||
onOkay();
|
||||
},
|
||||
onPressed: _onOkay,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onOkay() {
|
||||
Navigator.of(context).pop();
|
||||
void _onOkay() {
|
||||
Navi.popDialog(context);
|
||||
|
||||
final chiplets = _selectedEntries
|
||||
.map((e) => MessageFilterChiplet(
|
||||
label: _futureChannels?.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
||||
label: _futureChannels.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
||||
value: e,
|
||||
type: MessageFilterChipletType.channel,
|
||||
))
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class FilterModalKeytoken extends StatefulWidget {
|
||||
@override
|
||||
@@ -15,13 +17,12 @@ class FilterModalKeytoken extends StatefulWidget {
|
||||
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<KeyToken>>? _futureKeyTokens;
|
||||
ImmediateFuture<List<KeyToken>> _futureKeyTokens = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureKeyTokens = null;
|
||||
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
@@ -49,26 +50,22 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 9000,
|
||||
child: () {
|
||||
if (_futureKeyTokens == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
future: _futureKeyTokens!.future,
|
||||
child: FutureBuilder(
|
||||
future: _futureKeyTokens.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (_futureKeyTokens?.value != null) {
|
||||
return _buildList(context, _futureKeyTokens!.value!);
|
||||
if (_futureKeyTokens.value != null) {
|
||||
return _buildList(context, _futureKeyTokens.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||
return _buildList(context, snapshot.data!);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||
}
|
||||
}),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
@@ -83,11 +80,11 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
}
|
||||
|
||||
void onOkay() {
|
||||
Navigator.of(context).pop();
|
||||
Navi.popDialog(context);
|
||||
|
||||
final chiplets = _selectedEntries
|
||||
.map((e) => MessageFilterChiplet(
|
||||
label: _futureKeyTokens?.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
||||
label: _futureKeyTokens.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
||||
value: e,
|
||||
type: MessageFilterChipletType.sender,
|
||||
))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class FilterModalPriority extends StatefulWidget {
|
||||
@override
|
||||
@@ -31,7 +32,7 @@ class _FilterModalPriorityState extends State<FilterModalPriority> {
|
||||
return AlertDialog(
|
||||
title: const Text('Priority'),
|
||||
content: Container(
|
||||
width: 0,
|
||||
width: 9000,
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@@ -58,7 +59,7 @@ class _FilterModalPriorityState extends State<FilterModalPriority> {
|
||||
}
|
||||
|
||||
void onOkay() {
|
||||
Navigator.of(context).pop();
|
||||
Navi.popDialog(context);
|
||||
|
||||
final chiplets = _selectedEntries.map((e) => MessageFilterChiplet(label: _texts[e]?.$2 ?? '???', value: e, type: MessageFilterChipletType.priority)).toList();
|
||||
|
||||
|
||||
61
flutter/lib/components/modals/filter_modal_searchplain.dart
Normal file
61
flutter/lib/components/modals/filter_modal_searchplain.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class FilterModalSearchPlain extends StatefulWidget {
|
||||
@override
|
||||
_FilterModalSearchPlainState createState() => _FilterModalSearchPlainState();
|
||||
}
|
||||
|
||||
class _FilterModalSearchPlainState extends State<FilterModalSearchPlain> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Search'),
|
||||
content: Container(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(hintText: "Search..."),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Apply'),
|
||||
onPressed: _onOkay,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onOkay() {
|
||||
Navi.popDialog(context);
|
||||
|
||||
List<MessageFilterChiplet> chiplets = [];
|
||||
if (_controller.text.isNotEmpty) {
|
||||
chiplets.add(MessageFilterChiplet(
|
||||
label: _controller.text,
|
||||
value: _controller.text,
|
||||
type: MessageFilterChipletType.plainSearch,
|
||||
));
|
||||
}
|
||||
|
||||
AppEvents().notifyFilterListeners([MessageFilterChipletType.plainSearch], chiplets);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
@@ -14,18 +15,17 @@ class FilterModalSendername extends StatefulWidget {
|
||||
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<String>>? _futureSenders;
|
||||
ImmediateFuture<List<String>> _futureSenders = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureSenders = null;
|
||||
_futureSenders = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
|
||||
final senders = await APIClient.getSenderNameList(userAcc);
|
||||
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
||||
|
||||
return senders;
|
||||
}());
|
||||
@@ -48,40 +48,34 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 9000,
|
||||
child: () {
|
||||
if (_futureSenders == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
future: _futureSenders!.future,
|
||||
child: FutureBuilder(
|
||||
future: _futureSenders.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (_futureSenders?.value != null) {
|
||||
return _buildList(context, _futureSenders!.value!);
|
||||
if (_futureSenders.value != null) {
|
||||
return _buildList(context, _futureSenders.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||
return _buildList(context, snapshot.data!);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||
}
|
||||
}),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Apply'),
|
||||
onPressed: () {
|
||||
onOkay();
|
||||
},
|
||||
onPressed: _onOkay,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onOkay() {
|
||||
void _onOkay() {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final chiplets = _selectedEntries
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
|
||||
class FilterModalTime extends StatefulWidget {
|
||||
@override
|
||||
_FilterModalTimeState createState() => _FilterModalTimeState();
|
||||
}
|
||||
|
||||
class _FilterModalTimeState extends State<FilterModalTime> {
|
||||
DateTime? _tsBefore = null;
|
||||
DateTime? _tsAfter = null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Timerange'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 9000,
|
||||
child: Placeholder(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Apply'),
|
||||
onPressed: () {
|
||||
onOkay();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void onOkay() {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/nav_layout.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/main_messaging.dart';
|
||||
import 'package:simplecloudnotifier/main_utils.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/nav_layout.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/notifier.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'firebase_options.dart';
|
||||
|
||||
@@ -37,69 +28,16 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
print('[INIT] Init Globals...');
|
||||
|
||||
await Globals().init();
|
||||
|
||||
print('[INIT] Init Hive...');
|
||||
await initHive();
|
||||
|
||||
await Hive.initFlutter();
|
||||
print('[INIT] Register Hive-Adapters');
|
||||
await registerHiveAdapter();
|
||||
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
Hive.registerAdapter(SCNMessageAdapter());
|
||||
Hive.registerAdapter(ChannelAdapter());
|
||||
Hive.registerAdapter(FBMessageAdapter());
|
||||
|
||||
print('[INIT] Load Hive<scn-logs>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-logs');
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-requests>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-requests');
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-message-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-message-cache');
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-channel-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-channel-cache');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-fb-messages>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-fb-messages');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-fb-messages' + exc.toString(), trace: trace);
|
||||
}
|
||||
print('[INIT] Open Hive-Boxes');
|
||||
await openHiveBoxes(true);
|
||||
|
||||
print('[INIT] Load AppAuth...');
|
||||
|
||||
@@ -112,11 +50,13 @@ void main() async {
|
||||
await appAuth.loadUser();
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
try {
|
||||
await appAuth.loadClient();
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
}();
|
||||
}
|
||||
@@ -148,12 +88,14 @@ void main() async {
|
||||
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
|
||||
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
||||
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
|
||||
FirebaseMessaging.onMessage.listen(onForegroundMessage);
|
||||
} else {
|
||||
print('[INIT] Skip Firebase init (Platform == Linux)...');
|
||||
}
|
||||
|
||||
await appAuth.tryMigrateFromV1();
|
||||
|
||||
print('[INIT] Load Notifications...');
|
||||
|
||||
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
@@ -168,7 +110,7 @@ void main() async {
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
onDidReceiveLocalNotification: _receiveLocalDarwinNotification,
|
||||
onDidReceiveLocalNotification: receiveLocalDarwinNotification,
|
||||
notificationCategories: getDarwinNotificationCategories(),
|
||||
);
|
||||
final initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
@@ -179,23 +121,25 @@ void main() async {
|
||||
);
|
||||
flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: _receiveLocalNotification,
|
||||
onDidReceiveBackgroundNotificationResponse: _notificationTapBackground,
|
||||
onDidReceiveNotificationResponse: receiveLocalNotification,
|
||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
);
|
||||
|
||||
final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (appLaunchNotification != null) {
|
||||
// Use has launched SCN by clicking on a loca notifiaction, if it was a summary or message notifiaction open the corresponding screen
|
||||
// Use has launched SCN by clicking on a local notifiaction, if it was a summary or message notifiaction open the corresponding screen
|
||||
// This is android only
|
||||
//TODO same on iOS, somehow??
|
||||
ApplicationLog.info('App launched by notification: ${appLaunchNotification.notificationResponse?.id}');
|
||||
|
||||
_handleNotificationClickAction(appLaunchNotification.notificationResponse?.payload, Duration(milliseconds: 600));
|
||||
handleNotificationClickAction(appLaunchNotification.notificationResponse?.payload, Duration(milliseconds: 600));
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationLog.debug('[INIT] Application started');
|
||||
|
||||
Globals().appWidgetInitialized = true;
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
@@ -209,17 +153,56 @@ void main() async {
|
||||
);
|
||||
}
|
||||
|
||||
class SCNApp extends StatelessWidget {
|
||||
class SCNApp extends StatefulWidget {
|
||||
SCNApp({super.key});
|
||||
|
||||
static var materialKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
State<SCNApp> createState() => _SCNAppState();
|
||||
}
|
||||
|
||||
class _SCNAppState extends State<SCNApp> {
|
||||
StreamSubscription<List<PurchaseDetails>>? _purchaseSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (Globals().clientType == 'IOS' || Globals().clientType == 'ANDROID') {
|
||||
_purchaseSubscription = InAppPurchase.instance.purchaseStream.listen(
|
||||
purchaseUpdated,
|
||||
onDone: () => _purchaseSubscription?.cancel(),
|
||||
onError: purchaseError,
|
||||
);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseSubscription?.cancel();
|
||||
_purchaseSubscription = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void purchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
|
||||
// see https://pub.dev/packages/in_app_purchase
|
||||
|
||||
for (var purchaseDetails in purchaseDetailsList) {
|
||||
ApplicationLog.debug('Purchase ${purchaseDetails.productID} is ${purchaseDetails.status.toString()}'); //TODO
|
||||
}
|
||||
}
|
||||
|
||||
void purchaseError(dynamic error) {
|
||||
// TODO handle error here.
|
||||
ApplicationLog.error('Purchase error: ${error.toString()}', trace: StackTrace.current);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToastificationWrapper(
|
||||
config: ToastificationConfig(
|
||||
itemWidth: 440,
|
||||
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
|
||||
marginBuilder: (context, alignment) => EdgeInsets.symmetric(vertical: 64),
|
||||
animationDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
child: Consumer<AppTheme>(
|
||||
@@ -228,8 +211,10 @@ class SCNApp extends StatelessWidget {
|
||||
title: 'SimpleCloudNotifier',
|
||||
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
|
||||
theme: ThemeData(
|
||||
//TODO color settings
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: appTheme.color.value,
|
||||
brightness: appTheme.darkMode ? Brightness.dark : Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: SCNNavLayout(),
|
||||
@@ -238,172 +223,3 @@ class SCNApp extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void _notificationTapBackground(NotificationResponse notificationResponse) {
|
||||
// I think only iOS triggers this, TODO
|
||||
ApplicationLog.info('Received local notification<vm:entry-point>: ${notificationResponse.id}');
|
||||
}
|
||||
|
||||
void setFirebaseToken(String fcmToken) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
final oldToken = Globals().getPrefFCMToken();
|
||||
|
||||
await Globals().setPrefFCMToken(fcmToken);
|
||||
|
||||
ApplicationLog.info('New firebase token received', additional: 'Token: $fcmToken (old: $oldToken)');
|
||||
|
||||
if (!acc.isAuth()) return;
|
||||
|
||||
Client? client;
|
||||
try {
|
||||
client = await acc.loadClient(forceIfOlder: Duration(seconds: 60));
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldToken != null && oldToken == fcmToken && client != null && client.fcmToken == fcmToken) {
|
||||
ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
// should not really happen - perhaps someone externally deleted the client?
|
||||
final newClient = await APIClient.addClient(acc, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
|
||||
acc.setClientAndClientID(newClient);
|
||||
await acc.save();
|
||||
} else {
|
||||
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version);
|
||||
acc.setClientAndClientID(newClient);
|
||||
await acc.save();
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _onBackgroundMessage(RemoteMessage message) async {
|
||||
// a firebase message was received while the app was in the background or terminated
|
||||
await _receiveMessage(message, false);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void _onForegroundMessage(RemoteMessage message) {
|
||||
// a firebase message was received while the app was in the foreground
|
||||
_receiveMessage(message, true);
|
||||
}
|
||||
|
||||
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
||||
try {
|
||||
// ensure globals init
|
||||
if (!Globals().isInitialized) {
|
||||
print('[LATE-INIT] Init Globals() - to ensure working _receiveMessage($foreground)...');
|
||||
await Globals().init();
|
||||
}
|
||||
|
||||
// ensure hive init
|
||||
if (!Hive.isBoxOpen('scn-logs')) {
|
||||
print('[LATE-INIT] Init Hive - to ensure working _receiveMessage($foreground)...');
|
||||
|
||||
await Hive.initFlutter();
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
Hive.registerAdapter(SCNMessageAdapter());
|
||||
Hive.registerAdapter(ChannelAdapter());
|
||||
Hive.registerAdapter(FBMessageAdapter());
|
||||
}
|
||||
|
||||
print('[LATE-INIT] Ensure hive boxes are open for _receiveMessage($foreground)...');
|
||||
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
|
||||
|
||||
String scn_msg_id;
|
||||
|
||||
try {
|
||||
scn_msg_id = message.data['scn_msg_id'] as String;
|
||||
|
||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(int.parse(message.data['timestamp'] as String) * 1000);
|
||||
final title = message.data['title'] as String;
|
||||
final channel = message.data['channel'] as String;
|
||||
final channel_id = message.data['channel_id'] as String;
|
||||
final body = message.data['body'] as String;
|
||||
|
||||
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FBMessageLog.insert(message);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final msg = await APIClient.getMessage(AppAuth(), scn_msg_id);
|
||||
SCNDataCache().addToMessageCache([msg]);
|
||||
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to query+persist message: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {
|
||||
//TODO iOS?
|
||||
ApplicationLog.info('Received local notification<darwin>: $id -> [$title]');
|
||||
}
|
||||
|
||||
void _receiveLocalNotification(NotificationResponse details) {
|
||||
// User has tapped a flutter_local notification, while the app was running
|
||||
ApplicationLog.info('Tapped local notification: [[${details.id} | ${details.actionId} | ${details.input} | ${details.notificationResponseType} | ${details.payload}]]');
|
||||
|
||||
_handleNotificationClickAction(details.payload, Duration.zero);
|
||||
}
|
||||
|
||||
void _handleNotificationClickAction(String? payload, Duration delay) {
|
||||
final parts = payload?.split('\n') ?? [];
|
||||
|
||||
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
|
||||
final messageID = parts[1];
|
||||
() async {
|
||||
await Future.delayed(delay);
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
|
||||
Navi.push(SCNApp.materialKey.currentContext!, () => MessageViewPage(messageID: messageID, preloadedData: null));
|
||||
});
|
||||
}();
|
||||
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
|
||||
final channelID = parts[1];
|
||||
() async {
|
||||
await Future.delayed(delay);
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');
|
||||
Navi.push(SCNApp.materialKey.currentContext!, () => ChannelViewPage(channelID: channelID, preloadedData: null, needsReload: null));
|
||||
});
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
List<DarwinNotificationCategory> getDarwinNotificationCategories() {
|
||||
return <DarwinNotificationCategory>[
|
||||
//TODO ?!?
|
||||
];
|
||||
}
|
||||
|
||||
152
flutter/lib/main_messaging.dart
Normal file
152
flutter/lib/main_messaging.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/main.dart';
|
||||
import 'package:simplecloudnotifier/main_utils.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/notifier.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||
// I think only iOS triggers this, TODO
|
||||
ApplicationLog.info('Received local notification<vm:entry-point>: ${notificationResponse.id}');
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> onBackgroundMessage(RemoteMessage message) async {
|
||||
// a firebase message was received while the app was in the background or terminated
|
||||
await _receiveMessage(message, false);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void onForegroundMessage(RemoteMessage message) {
|
||||
// a firebase message was received while the app was in the foreground
|
||||
_receiveMessage(message, true);
|
||||
}
|
||||
|
||||
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
||||
try {
|
||||
// ensure globals init
|
||||
if (!Globals().isInitialized) {
|
||||
print('[LATE-INIT] Init Globals() - to ensure working _receiveMessage($foreground)...');
|
||||
await Globals().init();
|
||||
}
|
||||
|
||||
// ensure hive init
|
||||
if (!Globals().hiveInitialized) {
|
||||
print('[LATE-INIT] Init Hive - to ensure working _receiveMessage($foreground)...');
|
||||
await initHive();
|
||||
}
|
||||
|
||||
// ensure hive init
|
||||
if (!Globals().hiveAdaptersRegistered) {
|
||||
print('[LATE-INIT] Init Hive-Adapter - to ensure working _receiveMessage($foreground)...');
|
||||
await registerHiveAdapter();
|
||||
}
|
||||
|
||||
// ensure hive init
|
||||
if (!Globals().hiveBoxesOpened) {
|
||||
print('[LATE-INIT] Ensure hive boxes are open for _receiveMessage($foreground)...');
|
||||
await openHiveBoxes(false);
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
|
||||
ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
|
||||
|
||||
String scn_msg_id;
|
||||
|
||||
try {
|
||||
scn_msg_id = message.data['scn_msg_id'] as String;
|
||||
|
||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(int.parse(message.data['timestamp'] as String) * 1000);
|
||||
final title = message.data['title'] as String;
|
||||
final channel = message.data['channel'] as String;
|
||||
final channel_id = message.data['channel_id'] as String;
|
||||
final body = message.data['body'] as String;
|
||||
final prio = int.parse(message.data['priority'] as String);
|
||||
|
||||
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp, prio);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.writeRawFailure('Failed to decode received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
|
||||
ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FBMessageLog.insert(message);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.writeRawFailure('Failed to persist received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
|
||||
ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace);
|
||||
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final msg = await APIClient.getMessage(AppAuth(), scn_msg_id);
|
||||
SCNDataCache().addToMessageCache([msg]);
|
||||
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.writeRawFailure('Failed to query+persist message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
|
||||
ApplicationLog.error('Failed to query+persist message: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {
|
||||
//TODO iOS?
|
||||
ApplicationLog.info('Received local notification<darwin>: $id -> [$title]');
|
||||
}
|
||||
|
||||
void receiveLocalNotification(NotificationResponse details) {
|
||||
// User has tapped a flutter_local notification, while the app was running
|
||||
ApplicationLog.info('Tapped local notification: [[${details.id} | ${details.actionId} | ${details.input} | ${details.notificationResponseType} | ${details.payload}]]');
|
||||
|
||||
handleNotificationClickAction(details.payload, Duration.zero);
|
||||
}
|
||||
|
||||
void handleNotificationClickAction(String? payload, Duration delay) {
|
||||
final parts = payload?.split('\n') ?? [];
|
||||
|
||||
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
|
||||
final messageID = parts[1];
|
||||
() async {
|
||||
await Future.delayed(delay, () {});
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
|
||||
Navi.push(SCNApp.materialKey.currentContext!, () => MessageViewPage(messageID: messageID, preloadedData: null));
|
||||
});
|
||||
}();
|
||||
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
|
||||
final channelID = parts[1];
|
||||
() async {
|
||||
await Future.delayed(delay, () {});
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');
|
||||
Navi.push(SCNApp.materialKey.currentContext!, () => ChannelViewPage(channelID: channelID, preloadedData: null, needsReload: null));
|
||||
});
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
List<DarwinNotificationCategory> getDarwinNotificationCategories() {
|
||||
return <DarwinNotificationCategory>[
|
||||
//TODO ?!?
|
||||
];
|
||||
}
|
||||
155
flutter/lib/main_utils.dart
Normal file
155
flutter/lib/main_utils.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
|
||||
final lockHive = Mutex();
|
||||
|
||||
void setFirebaseToken(String fcmToken) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
final oldToken = Globals().getPrefFCMToken();
|
||||
|
||||
await Globals().setPrefFCMToken(fcmToken);
|
||||
|
||||
ApplicationLog.info('New firebase token received', additional: 'Token: $fcmToken (old: $oldToken)');
|
||||
|
||||
if (!acc.isAuth()) return;
|
||||
|
||||
Client? client;
|
||||
try {
|
||||
client = await acc.loadClient(forceIfOlder: Duration(seconds: 60));
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldToken != null && oldToken == fcmToken && client != null && client.fcmToken == fcmToken) {
|
||||
ApplicationLog.info('Firebase token unchanged - do nothing', additional: 'Token: $fcmToken');
|
||||
return;
|
||||
}
|
||||
|
||||
if (client == null) {
|
||||
// should not really happen - perhaps someone externally deleted the client?
|
||||
final newClient = await APIClient.addClient(acc, fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
|
||||
acc.setClientAndClientID(newClient);
|
||||
await acc.save();
|
||||
} else {
|
||||
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken: fcmToken, agentModel: Globals().deviceModel, name: Globals().hostname, agentVersion: Globals().version);
|
||||
acc.setClientAndClientID(newClient);
|
||||
await acc.save();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initHive() async {
|
||||
await lockHive.protect(() async {
|
||||
if (Globals().hiveAdaptersRegistered) return;
|
||||
|
||||
await Hive.initFlutter();
|
||||
Globals().hiveInitialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> openHiveBoxes(bool safe) async {
|
||||
await lockHive.protect(() async {
|
||||
if (Globals().hiveBoxesOpened) return;
|
||||
|
||||
if (!safe) {
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
||||
Globals().hiveBoxesOpened = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-logs>...');
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-logs');
|
||||
await Hive.openBox<SCNLog>('scn-logs');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-logs', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-requests>...');
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-requests');
|
||||
await Hive.openBox<SCNRequest>('scn-requests');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-requests', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-message-cache>...');
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-message-cache');
|
||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-message-cache', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-channel-cache>...');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-channel-cache');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-channel-cache', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-fb-messages>...');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-fb-messages');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-fb-messages' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
try {
|
||||
print('[INIT] Load Hive<scn-keytoken-value-cache>...');
|
||||
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-keytoken-value-cache');
|
||||
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-keytoken-value-cache' + exc.toString(), trace: trace);
|
||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-keytoken-value-cache', {'error': exc.toString(), 'trace': trace});
|
||||
}
|
||||
|
||||
Globals().hiveBoxesOpened = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> registerHiveAdapter() async {
|
||||
await lockHive.protect(() async {
|
||||
if (Globals().hiveAdaptersRegistered) return;
|
||||
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
Hive.registerAdapter(SCNMessageAdapter());
|
||||
Hive.registerAdapter(ChannelAdapter());
|
||||
Hive.registerAdapter(FBMessageAdapter());
|
||||
Hive.registerAdapter(KeyTokenAdapter());
|
||||
|
||||
Globals().hiveAdaptersRegistered = true;
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
class APIError {
|
||||
final bool success;
|
||||
final int error;
|
||||
final String errhighlight;
|
||||
final int errhighlight;
|
||||
final String message;
|
||||
|
||||
static final MISSING_UID = 1101;
|
||||
@@ -67,7 +67,7 @@ class APIError {
|
||||
return APIError(
|
||||
success: json['success'] as bool,
|
||||
error: (json['error'] as num).toInt(),
|
||||
errhighlight: json['errhighlight'] as String,
|
||||
errhighlight: (json['errhighlight'] as num).toInt(),
|
||||
message: json['message'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,13 +71,15 @@ class Channel extends HiveObject implements FieldDebuggable {
|
||||
];
|
||||
}
|
||||
|
||||
ChannelPreview toPreview() {
|
||||
ChannelPreview toPreview(Subscription? sub) {
|
||||
return ChannelPreview(
|
||||
channelID: this.channelID,
|
||||
ownerUserID: this.ownerUserID,
|
||||
internalName: this.internalName,
|
||||
displayName: this.displayName,
|
||||
descriptionName: this.descriptionName,
|
||||
messagesSent: this.messagesSent,
|
||||
subscription: sub,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +111,8 @@ class ChannelPreview {
|
||||
final String internalName;
|
||||
final String displayName;
|
||||
final String? descriptionName;
|
||||
final int messagesSent;
|
||||
final Subscription? subscription;
|
||||
|
||||
const ChannelPreview({
|
||||
required this.channelID,
|
||||
@@ -116,6 +120,8 @@ class ChannelPreview {
|
||||
required this.internalName,
|
||||
required this.displayName,
|
||||
required this.descriptionName,
|
||||
required this.messagesSent,
|
||||
required this.subscription,
|
||||
});
|
||||
|
||||
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
||||
@@ -125,6 +131,8 @@ class ChannelPreview {
|
||||
internalName: json['internal_name'] as String,
|
||||
displayName: json['display_name'] as String,
|
||||
descriptionName: json['description_name'] as String?,
|
||||
messagesSent: json['messages_sent'] as int,
|
||||
subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
part 'keytoken.g.dart';
|
||||
|
||||
@HiveType(typeId: 107)
|
||||
class KeyToken {
|
||||
@HiveField(0)
|
||||
final String keytokenID;
|
||||
|
||||
@HiveField(10)
|
||||
final String name;
|
||||
@HiveField(11)
|
||||
final String timestampCreated;
|
||||
final String? timestampLastused;
|
||||
@HiveField(13)
|
||||
final String? timestampLastUsed;
|
||||
@HiveField(14)
|
||||
final String ownerUserID;
|
||||
@HiveField(15)
|
||||
final bool allChannels;
|
||||
@HiveField(16)
|
||||
final List<String> channels;
|
||||
@HiveField(17)
|
||||
final String permissions;
|
||||
@HiveField(18)
|
||||
final int messagesSent;
|
||||
|
||||
const KeyToken({
|
||||
required this.keytokenID,
|
||||
required this.name,
|
||||
required this.timestampCreated,
|
||||
required this.timestampLastused,
|
||||
required this.timestampLastUsed,
|
||||
required this.ownerUserID,
|
||||
required this.allChannels,
|
||||
required this.channels,
|
||||
@@ -26,7 +41,7 @@ class KeyToken {
|
||||
keytokenID: json['keytoken_id'] as String,
|
||||
name: json['name'] as String,
|
||||
timestampCreated: json['timestamp_created'] as String,
|
||||
timestampLastused: json['timestamp_lastused'] as String?,
|
||||
timestampLastUsed: json['timestamp_lastused'] as String?,
|
||||
ownerUserID: json['owner_user_id'] as String,
|
||||
allChannels: json['all_channels'] as bool,
|
||||
channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
@@ -38,6 +53,34 @@ class KeyToken {
|
||||
static List<KeyToken> fromJsonArray(List<dynamic> jsonArr) {
|
||||
return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
KeyTokenPreview toPreview() {
|
||||
return KeyTokenPreview(
|
||||
keytokenID: keytokenID,
|
||||
name: name,
|
||||
ownerUserID: ownerUserID,
|
||||
allChannels: allChannels,
|
||||
channels: channels,
|
||||
permissions: permissions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeyTokenWithToken {
|
||||
final KeyToken keyToken;
|
||||
final String token;
|
||||
|
||||
KeyTokenWithToken({
|
||||
required this.keyToken,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
factory KeyTokenWithToken.fromJson(Map<String, dynamic> json) {
|
||||
return KeyTokenWithToken(
|
||||
keyToken: KeyToken.fromJson(json),
|
||||
token: json['token'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KeyTokenPreview {
|
||||
|
||||
65
flutter/lib/models/keytoken.g.dart
Normal file
65
flutter/lib/models/keytoken.g.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'keytoken.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class KeyTokenAdapter extends TypeAdapter<KeyToken> {
|
||||
@override
|
||||
final int typeId = 107;
|
||||
|
||||
@override
|
||||
KeyToken read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return KeyToken(
|
||||
keytokenID: fields[0] as String,
|
||||
name: fields[10] as String,
|
||||
timestampCreated: fields[11] as String,
|
||||
timestampLastUsed: fields[13] as String?,
|
||||
ownerUserID: fields[14] as String,
|
||||
allChannels: fields[15] as bool,
|
||||
channels: (fields[16] as List).cast<String>(),
|
||||
permissions: fields[17] as String,
|
||||
messagesSent: fields[18] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, KeyToken obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.keytokenID)
|
||||
..writeByte(10)
|
||||
..write(obj.name)
|
||||
..writeByte(11)
|
||||
..write(obj.timestampCreated)
|
||||
..writeByte(13)
|
||||
..write(obj.timestampLastUsed)
|
||||
..writeByte(14)
|
||||
..write(obj.ownerUserID)
|
||||
..writeByte(15)
|
||||
..write(obj.allChannels)
|
||||
..writeByte(16)
|
||||
..write(obj.channels)
|
||||
..writeByte(17)
|
||||
..write(obj.permissions)
|
||||
..writeByte(18)
|
||||
..write(obj.messagesSent);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is KeyTokenAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
85
flutter/lib/models/scan_result.dart
Normal file
85
flutter/lib/models/scan_result.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
|
||||
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel, Error }
|
||||
|
||||
abstract class ScanResult {
|
||||
ScanResultMode get mode;
|
||||
|
||||
static ScanResult? parse(String v) {
|
||||
var lines = v.split('\n');
|
||||
|
||||
if (lines.length == 1 && lines[0].startsWith('https://simplecloudnotifier.de?')) {
|
||||
final v = Uri.tryParse(lines[0]);
|
||||
|
||||
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
|
||||
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key'], url: lines[0]);
|
||||
}
|
||||
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
|
||||
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null, url: lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length == 6 && lines[0] == '@scn.channel.subscribe' && lines[1] == 'v1') {
|
||||
return ScanResultChannelSubscribe(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4], subscribeKey: lines[5]);
|
||||
}
|
||||
|
||||
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
|
||||
if (lines.length != 5) return null;
|
||||
|
||||
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
||||
}
|
||||
|
||||
return ScanResultError(message: 'Invalid QR code');
|
||||
}
|
||||
|
||||
static String createChannelQR(Channel channel) {
|
||||
return '@scn.channel' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID;
|
||||
}
|
||||
|
||||
static String createChannelSubscribeQR(Channel channel, String subscribeKey) {
|
||||
return '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID + '\n' + subscribeKey;
|
||||
}
|
||||
}
|
||||
|
||||
class ScanResultMessageSend extends ScanResult {
|
||||
final String userID;
|
||||
final String? userKey;
|
||||
final String url;
|
||||
|
||||
ScanResultMessageSend({required this.userID, required this.userKey, required this.url});
|
||||
|
||||
@override
|
||||
ScanResultMode get mode => ScanResultMode.MessageSend;
|
||||
}
|
||||
|
||||
class ScanResultChannel extends ScanResult {
|
||||
final String channelDisplayName;
|
||||
final String ownerUserID;
|
||||
final String channelID;
|
||||
|
||||
ScanResultChannel({required this.channelDisplayName, required this.ownerUserID, required this.channelID});
|
||||
|
||||
@override
|
||||
ScanResultMode get mode => ScanResultMode.Channel;
|
||||
}
|
||||
|
||||
class ScanResultChannelSubscribe extends ScanResult {
|
||||
final String channelDisplayName;
|
||||
final String ownerUserID;
|
||||
final String channelID;
|
||||
final String subscribeKey;
|
||||
|
||||
ScanResultChannelSubscribe({required this.channelDisplayName, required this.ownerUserID, required this.channelID, required this.subscribeKey});
|
||||
|
||||
@override
|
||||
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
|
||||
}
|
||||
|
||||
class ScanResultError extends ScanResult {
|
||||
final String message;
|
||||
|
||||
ScanResultError({required this.message});
|
||||
|
||||
@override
|
||||
ScanResultMode get mode => ScanResultMode.Error;
|
||||
}
|
||||
55
flutter/lib/models/send_message_response.dart
Normal file
55
flutter/lib/models/send_message_response.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
class SendMessageResponse {
|
||||
final bool success;
|
||||
final int errorID;
|
||||
final int errorHighlight;
|
||||
final String message;
|
||||
final bool suppressSend;
|
||||
final int messageCount;
|
||||
final int quota;
|
||||
final bool isPro;
|
||||
final int quotaMax;
|
||||
final String scnMessageID;
|
||||
|
||||
SendMessageResponse({
|
||||
required this.success,
|
||||
required this.errorID,
|
||||
required this.errorHighlight,
|
||||
required this.message,
|
||||
required this.suppressSend,
|
||||
required this.messageCount,
|
||||
required this.quota,
|
||||
required this.isPro,
|
||||
required this.quotaMax,
|
||||
required this.scnMessageID,
|
||||
});
|
||||
|
||||
factory SendMessageResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SendMessageResponse(
|
||||
success: json['success'] as bool,
|
||||
errorID: json['error'] as int,
|
||||
errorHighlight: json['errhighlight'] as int,
|
||||
message: json['message'] as String,
|
||||
suppressSend: json['suppress_send'] as bool,
|
||||
messageCount: json['messagecount'] as int,
|
||||
quota: json['quota'] as int,
|
||||
isPro: json['is_pro'] as bool,
|
||||
quotaMax: json['quota_max'] as int,
|
||||
scnMessageID: json['scn_msg_id'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'error': errorID,
|
||||
'errhighlight': errorHighlight,
|
||||
'message': message,
|
||||
'suppress_send': suppressSend,
|
||||
'messagecount': messageCount,
|
||||
'quota': quota,
|
||||
'is_pro': isPro,
|
||||
'quota_max': quotaMax,
|
||||
'scn_msg_id': scnMessageID,
|
||||
};
|
||||
}
|
||||
}
|
||||
26
flutter/lib/models/sender_name_statistics.dart
Normal file
26
flutter/lib/models/sender_name_statistics.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
class SenderNameStatistics {
|
||||
final String name;
|
||||
final String lastTimestamp;
|
||||
final String firstTimestamp;
|
||||
final int count;
|
||||
|
||||
const SenderNameStatistics({
|
||||
required this.name,
|
||||
required this.lastTimestamp,
|
||||
required this.firstTimestamp,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
factory SenderNameStatistics.fromJson(Map<String, dynamic> json) {
|
||||
return SenderNameStatistics(
|
||||
name: json['name'] as String,
|
||||
lastTimestamp: json['last_timestamp'] as String,
|
||||
firstTimestamp: json['first_timestamp'] as String,
|
||||
count: json['count'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
static List<SenderNameStatistics> fromJsonArray(List<dynamic> jsonArr) {
|
||||
return jsonArr.map<SenderNameStatistics>((e) => SenderNameStatistics.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ class Subscription {
|
||||
final String channelInternalName;
|
||||
final String timestampCreated;
|
||||
final bool confirmed;
|
||||
final bool active;
|
||||
|
||||
const Subscription({
|
||||
required this.subscriptionID,
|
||||
@@ -15,6 +16,7 @@ class Subscription {
|
||||
required this.channelInternalName,
|
||||
required this.timestampCreated,
|
||||
required this.confirmed,
|
||||
required this.active,
|
||||
});
|
||||
|
||||
factory Subscription.fromJson(Map<String, dynamic> json) {
|
||||
@@ -26,6 +28,7 @@ class Subscription {
|
||||
channelInternalName: json['channel_internal_name'] as String,
|
||||
timestampCreated: json['timestamp_created'] as String,
|
||||
confirmed: json['confirmed'] as bool,
|
||||
active: json['active'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:action_slider/action_slider.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/account/login.dart';
|
||||
import 'package:simplecloudnotifier/pages/account/show_token_modal.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
|
||||
import 'package:simplecloudnotifier/pages/client_list/client_list.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list.dart';
|
||||
import 'package:simplecloudnotifier/pages/sender_list/sender_list.dart';
|
||||
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
@@ -27,12 +39,13 @@ class AccountRootPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountRootPageState extends State<AccountRootPage> {
|
||||
late ImmediateFuture<int>? futureSubscriptionCount;
|
||||
late ImmediateFuture<int>? futureClientCount;
|
||||
late ImmediateFuture<int>? futureKeyCount;
|
||||
late ImmediateFuture<int>? futureChannelAllCount;
|
||||
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||
late ImmediateFuture<User>? futureUser;
|
||||
ImmediateFuture<int> _futureSubscriptionCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureClientCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureKeyCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureChannelAllCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureChannelOwnedCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureSenderNamesCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<User> _futureUser = ImmediateFuture.ofPending();
|
||||
|
||||
late AppAuth userAcc;
|
||||
|
||||
@@ -82,44 +95,51 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
}
|
||||
|
||||
void _createFutures() {
|
||||
futureSubscriptionCount = null;
|
||||
futureClientCount = null;
|
||||
futureKeyCount = null;
|
||||
futureChannelAllCount = null;
|
||||
futureChannelSubscribedCount = null;
|
||||
_futureSubscriptionCount = ImmediateFuture.ofPending();
|
||||
_futureClientCount = ImmediateFuture.ofPending();
|
||||
_futureKeyCount = ImmediateFuture.ofPending();
|
||||
_futureChannelAllCount = ImmediateFuture.ofPending();
|
||||
_futureChannelOwnedCount = ImmediateFuture.ofPending();
|
||||
_futureSenderNamesCount = ImmediateFuture.ofPending();
|
||||
|
||||
if (userAcc.isAuth()) {
|
||||
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||
_futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||
return channels.length;
|
||||
}());
|
||||
|
||||
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async {
|
||||
_futureChannelOwnedCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned);
|
||||
return channels.length;
|
||||
}());
|
||||
|
||||
futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||
_futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||
final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL);
|
||||
return subs.length;
|
||||
}());
|
||||
|
||||
futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||
_futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final clients = await APIClient.getClientList(userAcc);
|
||||
return clients.length;
|
||||
}());
|
||||
|
||||
futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||
_futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||
return keys.length;
|
||||
}());
|
||||
|
||||
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||
_futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
||||
return senders.length;
|
||||
}());
|
||||
|
||||
_futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +153,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
// refresh all data and then replace teh futures used in build()
|
||||
|
||||
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||
final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||
final subs = await APIClient.getSubscriptionList(userAcc, SubscriptionFilter.ALL);
|
||||
final clients = await APIClient.getClientList(userAcc);
|
||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||
final senderNames = await APIClient.getSenderNameList(userAcc);
|
||||
final user = await userAcc.loadUser(force: true);
|
||||
|
||||
setState(() {
|
||||
futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
||||
futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length);
|
||||
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||
futureUser = ImmediateFuture.ofValue(user);
|
||||
_futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
||||
_futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||
_futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||
_futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||
_futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
||||
_futureUser = ImmediateFuture.ofValue(user);
|
||||
});
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to refresh account data');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to refresh account data');
|
||||
@@ -166,12 +189,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
return _buildNoAuth(context);
|
||||
} else {
|
||||
return FutureBuilder(
|
||||
future: futureUser!.future,
|
||||
future: _futureUser.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (futureUser?.value != null) {
|
||||
return _buildShowAccount(context, acc, futureUser!.value!);
|
||||
if (_futureUser.value != null) {
|
||||
return _buildShowAccount(context, acc, _futureUser.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return _buildShowAccount(context, acc, snapshot.data!);
|
||||
} else {
|
||||
@@ -250,7 +273,20 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
_buildHeader(context, user),
|
||||
const SizedBox(height: 16),
|
||||
Text(user.username ?? user.userID, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
if (acc.tokenSend != null || acc.tokenAdmin != null)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'UserID & Token kopieren',
|
||||
onPressed: () => _showTokenModal(context, acc),
|
||||
icon: FontAwesomeIcons.copy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
..._buildCards(context, user),
|
||||
SizedBox(height: 16),
|
||||
_buildFooter(context, user),
|
||||
@@ -328,10 +364,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
children: [
|
||||
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
||||
FutureBuilder(
|
||||
future: futureChannelAllCount!.future,
|
||||
future: _futureChannelOwnedCount.future,
|
||||
builder: (context, snapshot) {
|
||||
if (futureChannelAllCount?.value != null) {
|
||||
return Text('${futureChannelAllCount!.value}');
|
||||
if (_futureChannelOwnedCount.value != null) {
|
||||
return Text('${_futureChannelOwnedCount.value}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(color: Colors.red));
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}');
|
||||
} else {
|
||||
@@ -348,13 +386,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
UI.buttonIconOnly(
|
||||
onPressed: () {/*TODO*/},
|
||||
onPressed: _changeUsername,
|
||||
icon: FontAwesomeIcons.pen,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (!user.isPro)
|
||||
UI.buttonIconOnly(
|
||||
onPressed: () {/*TODO*/},
|
||||
onPressed: () {
|
||||
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
|
||||
},
|
||||
icon: FontAwesomeIcons.cartCircleArrowUp,
|
||||
),
|
||||
],
|
||||
@@ -365,10 +405,11 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
|
||||
List<Widget> _buildCards(BuildContext context, User user) {
|
||||
return [
|
||||
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Subscription', 's', _futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
|
||||
_buildNumberCard(context, 'Client', 's', _futureClientCount, () => Navi.push(context, () => ClientListPage())),
|
||||
_buildNumberCard(context, 'Key', 's', _futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
|
||||
_buildNumberCard(context, 'Channel', 's', _futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
|
||||
_buildNumberCard(context, 'Sender', '', _futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
@@ -379,22 +420,32 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
onTap: () {
|
||||
Navi.push(
|
||||
context,
|
||||
() => FilteredMessageViewPage(
|
||||
title: "All Messages",
|
||||
alertText: "All messages sent from your account",
|
||||
filter: MessageFilter(senderUserID: [user.userID]),
|
||||
));
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildNumberCard(BuildContext context, String txt, ImmediateFuture<int>? future, void Function() action) {
|
||||
Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int> future, void Function() action) {
|
||||
return UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: future?.future,
|
||||
future: future.future,
|
||||
builder: (context, snapshot) {
|
||||
if (future?.value != null) {
|
||||
return Text('${future?.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
if (future.value != null) {
|
||||
return Text('${future.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else {
|
||||
@@ -403,7 +454,20 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(txt, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
FutureBuilder(
|
||||
future: future.future,
|
||||
builder: (context, snapshot) {
|
||||
if (future.value != null) {
|
||||
return Text('${txt}${((future.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else {
|
||||
return Text(txt, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: action,
|
||||
@@ -420,14 +484,16 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
text: 'Logout',
|
||||
onPressed: _logout,
|
||||
color: Colors.orange,
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Delete Account',
|
||||
onPressed: _deleteAccount,
|
||||
color: Colors.red,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -467,9 +533,17 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
|
||||
await acc.save();
|
||||
Toaster.success("Success", 'Successfully Created a new account');
|
||||
} catch (exc, trace) {
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ShowTokenModal(account: acc, isAfterRegister: true),
|
||||
);
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to create user account');
|
||||
ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to create user account');
|
||||
ApplicationLog.error('Failed to create user account: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
@@ -486,6 +560,208 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
}
|
||||
|
||||
void _deleteAccount() async {
|
||||
//TODO
|
||||
final acc = AppAuth();
|
||||
if (!acc.isAuth()) return;
|
||||
|
||||
final r1 = await UIDialogs.showConfirmDialog(context, "Delete Account?", text: "Are you sure you want to delete your account?", okText: "Delete", cancelText: "Cancel");
|
||||
if (!r1) return;
|
||||
|
||||
final r2 = await UIDialogs.showConfirmDialog(context, "Really sure?", text: "Are you really sure you want to delete your account?.\n\nThis includes all your messages and channels etc.\nThis action cannot be undone!", okText: "Delete", cancelText: "Cancel");
|
||||
if (!r2) return;
|
||||
|
||||
final r3 = await UIDialogs.showTextInput(context, "Please input 'Delete' to delete your Account.", "");
|
||||
if (r3 == null) return;
|
||||
if (r3.trim().toLowerCase() != 'delete') return;
|
||||
|
||||
final r4 = await this._showDeleteDialog(context);
|
||||
if (!r4) return;
|
||||
|
||||
final r5 = await this._showDeleteAccountWaitDialog(context);
|
||||
if (!r5) return;
|
||||
|
||||
try {
|
||||
await APIClient.deleteUser(acc, acc.userID!);
|
||||
|
||||
Toaster.info('Logout', 'Successfully logged out');
|
||||
|
||||
//TODO clear messages/channels/etc in open views
|
||||
acc.clear();
|
||||
await acc.save();
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to delete user');
|
||||
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to delete user');
|
||||
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _changeUsername() async {
|
||||
final acc = AppAuth();
|
||||
if (!acc.isAuth()) return;
|
||||
|
||||
var newusername = await UIDialogs.showTextInput(context, 'Change your public username', 'Enter new username');
|
||||
if (newusername == null) return;
|
||||
|
||||
newusername = newusername.trim();
|
||||
if (newusername == '') {
|
||||
Toaster.error("Error", 'Username cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
|
||||
setState(() {
|
||||
_futureUser = ImmediateFuture.ofValue(user);
|
||||
});
|
||||
Toaster.success("Success", 'Username changed');
|
||||
|
||||
_backgroundRefresh();
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to update username');
|
||||
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to update username');
|
||||
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteDialog(BuildContext context) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("Delete Account?"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ActionSlider.standard(
|
||||
sliderBehavior: SliderBehavior.stretch,
|
||||
width: 300,
|
||||
backgroundColor: Colors.white,
|
||||
toggleColor: Colors.red,
|
||||
action: (controller) => Navigator.of(context).pop(true),
|
||||
child: const Text('Slide to delete'),
|
||||
),
|
||||
width: 300,
|
||||
height: 65,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((value) => value ?? false);
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteAccountWaitDialog(BuildContext context) {
|
||||
final completer = Completer<bool>();
|
||||
|
||||
bool isTimerActive = true;
|
||||
|
||||
const int totalSeconds = 20;
|
||||
int secondsRemaining = totalSeconds;
|
||||
double percentageRemaining = 1.0;
|
||||
|
||||
late Timer timer;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
void Function(void Function())? setStateInner = null;
|
||||
|
||||
final t0 = DateTime.now();
|
||||
|
||||
timer = Timer.periodic(const Duration(milliseconds: 50), (t) {
|
||||
setStateInner?.call(() {
|
||||
percentageRemaining = 1 - (DateTime.now().millisecondsSinceEpoch - t0.millisecondsSinceEpoch) / (totalSeconds * 1000.0);
|
||||
secondsRemaining = (totalSeconds * percentageRemaining).ceil();
|
||||
|
||||
if (secondsRemaining <= 0) {
|
||||
t.cancel();
|
||||
isTimerActive = false;
|
||||
|
||||
// Close the dialog and return true for successful deletion
|
||||
Navigator.of(dialogContext).pop();
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
setStateInner = setState;
|
||||
return AlertDialog(
|
||||
title: const Text('Delete Account'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Your account will be deleted in $secondsRemaining seconds.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: CircularProgressIndicator(
|
||||
value: 1 - percentageRemaining,
|
||||
strokeWidth: 8.0,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.red),
|
||||
backgroundColor: Colors.grey[300],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Cancel the timer
|
||||
if (timer.isActive) {
|
||||
timer.cancel();
|
||||
}
|
||||
isTimerActive = false;
|
||||
|
||||
// Close the dialog and return false for cancellation
|
||||
Navigator.of(dialogContext).pop();
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
},
|
||||
child: const Text('CANCEL'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
).then((_) {
|
||||
// Ensure timer is cancelled if dialog is dismissed
|
||||
if (timer.isActive) {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
// If the completer hasn't been completed yet (e.g., if the dialog is dismissed),
|
||||
// complete it with false
|
||||
if (!completer.isCompleted && isTimerActive) {
|
||||
completer.complete(false);
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _showTokenModal(BuildContext context, AppAuth acc) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => ShowTokenModal(account: acc, isAfterRegister: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
@@ -122,9 +123,9 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
try {
|
||||
setState(() => loading = true);
|
||||
|
||||
final uid = _ctrlUserID.text;
|
||||
final atokv = _ctrlTokenAdmin.text;
|
||||
final stokv = _ctrlTokenSend.text;
|
||||
var uid = _ctrlUserID.text;
|
||||
var atokv = _ctrlTokenAdmin.text;
|
||||
var stokv = _ctrlTokenSend.text;
|
||||
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
|
||||
@@ -140,12 +141,18 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stokv != "") {
|
||||
final toks = await APIClient.getKeyTokenByToken(uid, stokv);
|
||||
|
||||
if (!toks.allChannels || toks.permissions != 'CS') {
|
||||
Toaster.error("Error", 'Send token does not have required permissions');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final toks = await APIClient.createKeyToken(DirectTokenSource(uid, atokv), "SendKey (auto generated by SCN)", "CS", true);
|
||||
|
||||
stokv = toks.token;
|
||||
}
|
||||
|
||||
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
|
||||
|
||||
@@ -156,9 +163,12 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
|
||||
Toaster.success("Login", "Successfully logged in");
|
||||
Navi.popToRoot(context);
|
||||
} catch (exc, trace) {
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to verify token');
|
||||
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to verify token');
|
||||
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
|
||||
84
flutter/lib/pages/account/show_token_modal.dart
Normal file
84
flutter/lib/pages/account/show_token_modal.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class ShowTokenModal extends StatelessWidget {
|
||||
final AppAuth account;
|
||||
final bool isAfterRegister;
|
||||
|
||||
const ShowTokenModal({Key? key, required this.account, required this.isAfterRegister}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var alertText = "Use your UserID and Send-Token to send messages to your account.\n\nThe Admin-Token should not be shared.";
|
||||
if (this.isAfterRegister) {
|
||||
alertText = "These are your UserID and tokens.\n\nBackup your Admin-Token safely.\n\nUse UserId & Send-Token to send yourself messages.";
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('UserID & Token'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 450,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: alertText,
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (this.account.userID != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'UserID',
|
||||
values: [this.account.userID!],
|
||||
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('UserID', this.account.userID!))],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (this.account.tokenSend != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidKey,
|
||||
title: 'Send-Token',
|
||||
values: [this.account.tokenSend!],
|
||||
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('Send-Token', this.account.tokenSend!))],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (this.account.tokenSend != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidKey,
|
||||
title: 'Admin-Token',
|
||||
values: [this.account.tokenAdmin!],
|
||||
iconActions: [(FontAwesomeIcons.copy, null, () => _copy('Admin-Token', this.account.tokenAdmin!))],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Close'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _copy(String txt, String v) {
|
||||
Clipboard.setData(new ClipboardData(text: v));
|
||||
Toaster.info("Clipboard", 'Copied ${txt} to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
@@ -144,7 +144,19 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: RefreshIndicator(
|
||||
body: _buildList(context),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'fab_channel_list_qr',
|
||||
onPressed: () {
|
||||
Navi.push(context, () => ChannelScannerPage());
|
||||
},
|
||||
child: const Icon(FontAwesomeIcons.qrcode),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
@@ -154,20 +166,17 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
||||
itemBuilder: (context, item, index) => ChannelListItem(
|
||||
channel: item.channel,
|
||||
subscription: item.subscription,
|
||||
onPressed: () {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: item.channel.channelID, preloadedData: (item.channel, item.subscription), needsReload: _enqueueReload));
|
||||
mode: ChannelListItemMode.Messages,
|
||||
onChannelListReloadTrigger: _enqueueReload,
|
||||
onSubscriptionChanged: (channelID, subscription) {
|
||||
setState(() {
|
||||
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
|
||||
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'fab_channel_list_qr',
|
||||
onPressed: () {
|
||||
//TODO scan qr code to subscribe channel
|
||||
},
|
||||
child: const Icon(FontAwesomeIcons.qrcode),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
181
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
181
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class ChannelListExtendedPage extends StatefulWidget {
|
||||
const ChannelListExtendedPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChannelListExtendedPage> createState() => _ChannelListExtendedPageState();
|
||||
}
|
||||
|
||||
class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with RouteAware {
|
||||
final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
bool _reloadEnqueued = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('ChannelRootPage::dispose');
|
||||
_pagingController.dispose();
|
||||
Navi.modalRouteObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
if (_reloadEnqueued) {
|
||||
ApplicationLog.debug('[ChannelList::RouteObserver] --> didPopNext (will background-refresh) (_reloadEnqueued == true)');
|
||||
() async {
|
||||
_reloadEnqueued = false;
|
||||
await Future.delayed(const Duration(milliseconds: 500), () {}); // prevents flutter bug where the whole process crashes ?!?
|
||||
await _backgroundRefresh();
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start background refresh of channel list');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
|
||||
|
||||
setState(() {
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
});
|
||||
} catch (exc, trace) {
|
||||
setState(() {
|
||||
_pagingController.error = exc.toString();
|
||||
});
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Channels",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: "All channels accessible from this account\n(Your own channels and subscribed channels)",
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildList(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'fab_channel_list_extended-plus',
|
||||
onPressed: () {
|
||||
Navi.push(context, () => ChannelScannerPage());
|
||||
},
|
||||
child: const Icon(FontAwesomeIcons.plus),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<int, ChannelWithSubscription>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
|
||||
itemBuilder: (context, item, index) => ChannelListItem(
|
||||
channel: item.channel,
|
||||
subscription: item.subscription,
|
||||
mode: ChannelListItemMode.Extended,
|
||||
onChannelListReloadTrigger: _enqueueReload,
|
||||
onSubscriptionChanged: (channelID, subscription) {
|
||||
setState(() {
|
||||
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
|
||||
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueReload() {
|
||||
_reloadEnqueued = true;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,40 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
class ChannelListItem extends StatefulWidget {
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
enum ChannelListItemMode {
|
||||
Messages,
|
||||
Extended,
|
||||
}
|
||||
|
||||
class ChannelListItem extends StatefulWidget {
|
||||
const ChannelListItem({
|
||||
required this.channel,
|
||||
required this.onPressed,
|
||||
required this.onChannelListReloadTrigger,
|
||||
required this.onSubscriptionChanged,
|
||||
required this.subscription,
|
||||
required this.mode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Channel channel;
|
||||
final Subscription? subscription;
|
||||
final Null Function() onPressed;
|
||||
final void Function() onChannelListReloadTrigger;
|
||||
final ChannelListItemMode mode;
|
||||
final void Function(String, Subscription?) onSubscriptionChanged;
|
||||
|
||||
@override
|
||||
State<ChannelListItem> createState() => _ChannelListItemState();
|
||||
@@ -41,11 +49,11 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (acc.isAuth()) {
|
||||
if (acc.isAuth() && widget.mode == ChannelListItemMode.Messages) {
|
||||
lastMessage = SCNDataCache().getMessagesSorted().where((p) => p.channelID == widget.channel.channelID).firstOrNull;
|
||||
|
||||
() async {
|
||||
final (_, channelMessages) = await APIClient.getMessageList(acc, '@start', pageSize: 1, filter: MessageFilter(channelIDs: [widget.channel.channelID]));
|
||||
final (_, channelMessages) = await APIClient.getChannelMessageList(acc, widget.channel.channelID, '@start', pageSize: 1);
|
||||
setState(() {
|
||||
lastMessage = channelMessages.firstOrNull;
|
||||
});
|
||||
@@ -55,13 +63,20 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//TODO subscription status
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onTap: () {
|
||||
if (widget.mode == ChannelListItemMode.Messages) {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
|
||||
} else {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
@@ -81,7 +96,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
|
||||
(widget.channel.timestampLastSent == null) ? '' : dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
@@ -90,13 +105,8 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_preformatTitle(lastMessage),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
),
|
||||
),
|
||||
Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
Expanded(child: (widget.mode == ChannelListItemMode.Messages) ? Text(_preformatTitle(lastMessage), style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))) : _buildSubscriptionStateText(context)),
|
||||
(widget.mode == ChannelListItemMode.Messages) ? Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)) : Text("", style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -105,11 +115,15 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: this.widget.channel));
|
||||
if (widget.mode == ChannelListItemMode.Messages) {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
|
||||
} else {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
child: (widget.mode == ChannelListItemMode.Messages) ? Icon(FontAwesomeIcons.solidSquareInfo, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24) : Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -125,14 +139,158 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
}
|
||||
|
||||
Widget _buildIcon(BuildContext context) {
|
||||
if (widget.subscription == null) {
|
||||
return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
||||
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
return Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
|
||||
} else if (widget.subscription!.confirmed) {
|
||||
return Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
|
||||
final acc = AppAuth();
|
||||
|
||||
if (widget.subscription == null && widget.channel.ownerUserID == acc.userID) {
|
||||
// not-subscribed (own channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
||||
result = GestureDetector(onTap: () => _subscribe(), child: result);
|
||||
return result;
|
||||
} else if (widget.subscription == null) {
|
||||
// not-subscribed (foreign channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
||||
return result;
|
||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
|
||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
// inactive (own channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
||||
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
|
||||
return result;
|
||||
} else {
|
||||
return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
||||
// inactive (foreign channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
||||
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
|
||||
return result;
|
||||
}
|
||||
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
|
||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
// subscribed+active (own channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
|
||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||
return result;
|
||||
} else {
|
||||
// subscribed+active (foreign channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
|
||||
result = GestureDetector(onTap: () => _deactivate(widget.subscription!), child: result);
|
||||
return result;
|
||||
}
|
||||
} else if (!widget.subscription!.confirmed) {
|
||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
// requested (own channel)
|
||||
return SizedBox(width: 32, height: 32);
|
||||
} else {
|
||||
// requested (foreign channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32);
|
||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
return SizedBox(width: 32, height: 32);
|
||||
}
|
||||
|
||||
Widget _buildSubscriptionStateText(BuildContext context) {
|
||||
if (widget.subscription == null) {
|
||||
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed && widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
return Text("inactive (own channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
|
||||
return Text("subscribed & active (foreign channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
|
||||
return Text("subscribed (foreign channel) (inactive)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else {
|
||||
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribe() async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID()) {
|
||||
try {
|
||||
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channel.channelID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged(widget.channel.channelID, sub);
|
||||
|
||||
if (sub.confirmed) {
|
||||
Toaster.success("Success", 'Subscribed to channel');
|
||||
} else {
|
||||
Toaster.success("Success", 'Requested widget.subscription to channel');
|
||||
}
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _unsubscribe(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth()) {
|
||||
try {
|
||||
await APIClient.deleteSubscription(acc, sub.channelID, sub.subscriptionID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged.call(sub.channelID, null);
|
||||
|
||||
Toaster.success("Success", 'Unsubscribed from channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _deactivate(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth()) {
|
||||
try {
|
||||
var newSub = await APIClient.deactivateSubscription(acc, sub.channelID, sub.subscriptionID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged.call(sub.channelID, newSub);
|
||||
|
||||
Toaster.success("Success", 'Unsubscribed from channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _activate(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth()) {
|
||||
try {
|
||||
var newSub = await APIClient.activateSubscription(acc, sub.channelID, sub.subscriptionID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged.call(sub.channelID, newSub);
|
||||
|
||||
Toaster.success("Success", 'Subscribed to channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
@@ -55,7 +55,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
|
||||
}
|
||||
|
||||
try {
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: MessageFilter(channelIDs: [this.widget.channel.channelID]));
|
||||
final (npt, newItems) = await APIClient.getChannelMessageList(acc, this.widget.channel.channelID, thisPageToken, pageSize: cfg.messagePageSize);
|
||||
|
||||
SCNDataCache().addToMessageCache(newItems); // no await
|
||||
|
||||
|
||||
171
flutter/lib/pages/channel_scanner/channel_scanner.dart
Normal file
171
flutter/lib/pages/channel_scanner/channel_scanner.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelview.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_messagesend.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class ChannelScannerPage extends StatefulWidget {
|
||||
const ChannelScannerPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChannelScannerPage> createState() => _ChannelScannerPageState();
|
||||
}
|
||||
|
||||
class _ChannelScannerPageState extends State<ChannelScannerPage> {
|
||||
final MobileScannerController _controller = MobileScannerController(
|
||||
formats: const [BarcodeFormat.qrCode],
|
||||
);
|
||||
|
||||
ScanResult? scanResult = null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Scanner",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
BadgeDisplay(
|
||||
text: "Scan the QR Code of an account to send messages to this account,\nor the QR Code of an channel to request a subscription to this channel.",
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
if (scanResult == null) ...[
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(UI.DefaultBorderRadius),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: MobileScanner(
|
||||
fit: BoxFit.cover,
|
||||
controller: _controller,
|
||||
onDetect: _handleBarcode,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
],
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 300, minWidth: 300, minHeight: 200),
|
||||
child: _buildScanResult(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleBarcode(BarcodeCapture barcodes) {
|
||||
setState(() {
|
||||
if (barcodes.barcodes.isEmpty) {
|
||||
scanResult = null;
|
||||
} else {
|
||||
scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? '');
|
||||
print('parsed: ${jsonEncode(barcodes.barcodes[0].rawValue)} as ${scanResult.runtimeType.toString()}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScanResult(BuildContext context) {
|
||||
if (scanResult == null) {
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||
context: context,
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(48)),
|
||||
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (scanResult! is ScanResultMessageSend) {
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
context: context,
|
||||
child: ChannelScannerResultMessageSend(value: scanResult! as ScanResultMessageSend),
|
||||
);
|
||||
}
|
||||
|
||||
if (scanResult! is ScanResultChannel) {
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
context: context,
|
||||
child: ChannelScannerResultChannelView(value: scanResult! as ScanResultChannel),
|
||||
);
|
||||
}
|
||||
|
||||
if (scanResult! is ScanResultChannelSubscribe) {
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
context: context,
|
||||
child: ChannelScannerResultChannelSubscribe(value: scanResult! as ScanResultChannelSubscribe),
|
||||
);
|
||||
}
|
||||
|
||||
if (scanResult! is ScanResultError) {
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||
context: context,
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||
Text((scanResult! as ScanResultError).message, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return UI.box(
|
||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||
context: context,
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class ChannelScannerResultChannelSubscribe extends StatefulWidget {
|
||||
final ScanResultChannelSubscribe value;
|
||||
|
||||
const ChannelScannerResultChannelSubscribe({required this.value}) : super();
|
||||
|
||||
@override
|
||||
State<ChannelScannerResultChannelSubscribe> createState() => _ChannelScannerResultChannelSubscribeState();
|
||||
}
|
||||
|
||||
class _ChannelScannerResultChannelSubscribeState extends State<ChannelScannerResultChannelSubscribe> {
|
||||
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
||||
|
||||
_ChannelScannerResultChannelSubscribeState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||
|
||||
Subscription? overrideSubscription = null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||
setState(() {
|
||||
_fetchDataFuture = _fetchData(auth);
|
||||
});
|
||||
}
|
||||
|
||||
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
||||
ChannelPreview? channel = null;
|
||||
try {
|
||||
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
UserPreview? user = null;
|
||||
try {
|
||||
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (channel, user);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
||||
future: _fetchDataFuture,
|
||||
builder: (context, snapshot) {
|
||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
}
|
||||
|
||||
if (snapshot.data == null) {
|
||||
return Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final (channel, user) = snapshot.data!;
|
||||
|
||||
final sub = overrideSubscription ?? channel.subscription;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text((user.username ?? user.userID) + ((auth.userID != null && auth.userID! == user.userID) ? "\n(you)" : "")), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(sub)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
if (sub == null)
|
||||
UI.button(
|
||||
text: 'Request Subscription',
|
||||
onPressed: _onSubscribe,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
if (sub != null && sub.confirmed)
|
||||
UI.button(
|
||||
text: 'Go to channel',
|
||||
onPressed: () {
|
||||
Navi.pushOnRoot(context, () => ChannelViewPage(channelID: widget.value.channelID, preloadedData: null, needsReload: null));
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubscribe() async {
|
||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||
try {
|
||||
var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey);
|
||||
if (sub.confirmed) {
|
||||
Toaster.success("Success", "Subscription request sent and auto-confirmed");
|
||||
} else {
|
||||
Toaster.success("Success", "Subscription request sent - pending confirmation");
|
||||
}
|
||||
setState(() {
|
||||
overrideSubscription = sub;
|
||||
});
|
||||
} catch (e) {
|
||||
Toaster.error("Error", 'Failed to send subscription-request: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
String _formatSubscriptionStatus(Subscription? sub) {
|
||||
if (sub == null) {
|
||||
return "Not Subscribed";
|
||||
} else if (sub.confirmed) {
|
||||
if (sub.active) {
|
||||
return "Already Subscribed";
|
||||
} else {
|
||||
return "Already Subscribed (inactive)";
|
||||
}
|
||||
} else {
|
||||
return "Unconfirmed Subscription";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
class ChannelScannerResultChannelView extends StatefulWidget {
|
||||
final ScanResultChannel value;
|
||||
|
||||
const ChannelScannerResultChannelView({required this.value}) : super();
|
||||
|
||||
@override
|
||||
State<ChannelScannerResultChannelView> createState() => _ChannelScannerResultChannelViewState();
|
||||
}
|
||||
|
||||
class _ChannelScannerResultChannelViewState extends State<ChannelScannerResultChannelView> {
|
||||
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
||||
|
||||
_ChannelScannerResultChannelViewState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||
setState(() {
|
||||
_fetchDataFuture = _fetchData(auth);
|
||||
});
|
||||
}
|
||||
|
||||
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
||||
ChannelPreview? channel = null;
|
||||
try {
|
||||
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
UserPreview? user = null;
|
||||
try {
|
||||
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (channel, user);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
||||
future: _fetchDataFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
}
|
||||
|
||||
if (snapshot.data == null) {
|
||||
return Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final (channel, user) = snapshot.data!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(user.username ?? user.userID), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(channel.subscription)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
Text('QR Code contains no subscription-key\nCannot subscribe to channel', textAlign: TextAlign.center, style: const TextStyle(fontStyle: FontStyle.italic)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatSubscriptionStatus(Subscription? sub) {
|
||||
if (sub == null) {
|
||||
return "Not Subscribed";
|
||||
} else if (sub.confirmed) {
|
||||
if (sub.active) {
|
||||
return "Already Subscribed";
|
||||
} else {
|
||||
return "Already Subscribed (inactive)";
|
||||
}
|
||||
} else {
|
||||
return "Unconfirmed Subscription";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ChannelScannerResultMessageSend extends StatefulWidget {
|
||||
final ScanResultMessageSend value;
|
||||
|
||||
const ChannelScannerResultMessageSend({required this.value}) : super();
|
||||
|
||||
@override
|
||||
State<ChannelScannerResultMessageSend> createState() => _ChannelScannerResultMessageSendState();
|
||||
}
|
||||
|
||||
class _ChannelScannerResultMessageSendState extends State<ChannelScannerResultMessageSend> {
|
||||
Future<(UserPreview, KeyTokenPreview?)?> _fetchDataFuture;
|
||||
|
||||
_ChannelScannerResultMessageSendState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||
|
||||
late TextEditingController _ctrlMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_ctrlMessage = TextEditingController();
|
||||
|
||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||
setState(() {
|
||||
_fetchDataFuture = _fetchData(auth);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrlMessage.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<(UserPreview, KeyTokenPreview?)?> _fetchData(AppAuth auth) async {
|
||||
UserPreview? user = null;
|
||||
try {
|
||||
user = await APIClient.getUserPreview(auth, widget.value.userID);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.userID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
KeyTokenPreview? key = null;
|
||||
if (widget.value.userKey != null) {
|
||||
try {
|
||||
key = await APIClient.getKeyTokenPreviewByToken(auth, widget.value.userKey!);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to fetch keytoken preview: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to fetch keytoken (preview) for ${widget.value.userID}', trace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (user, key);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<(UserPreview, KeyTokenPreview?)?>(
|
||||
future: _fetchDataFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
}
|
||||
|
||||
if (snapshot.data == null) {
|
||||
return Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final (user, key) = snapshot.data!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text((widget.value.userKey == null) ? "SCN User" : "SCN User & Key", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
if (user.username != null)
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(user.username!), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (key != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("KeyID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(key.keytokenID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(child: Text("KeyName:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(child: SingleChildScrollView(child: Text(key.name), scrollDirection: Axis.horizontal)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ConstrainedBox(child: Text("Permissions:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Text(_formatPermissions(key.permissions) + "\n" + (key.allChannels ? "(all channels)" : '(${key.channels.length} channels)')),
|
||||
scrollDirection: Axis.horizontal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
if (widget.value.userKey == null)
|
||||
Text(
|
||||
'QR Code contains no key\nCannot send messages',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
if (widget.value.userKey != null) ..._buildSend(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSend(BuildContext context) {
|
||||
return [
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _ctrlMessage,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Text',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Send Message',
|
||||
onPressed: _onSend,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
UI.buttonIconOnly(
|
||||
icon: FontAwesomeIcons.earthAmericas,
|
||||
onPressed: _onOpenWeb,
|
||||
square: true,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _onSend() async {
|
||||
if (_ctrlMessage.text.isEmpty) {
|
||||
Toaster.error("Error", 'Please enter a message');
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.value.userKey == null) return;
|
||||
|
||||
try {
|
||||
await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text);
|
||||
Toaster.success("Success", 'Message sent');
|
||||
setState(() {
|
||||
_ctrlMessage.clear();
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _onOpenWeb() async {
|
||||
try {
|
||||
final Uri uri = Uri.parse(widget.value.url);
|
||||
|
||||
ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]');
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
Toaster.error("Error", 'Cannot open URL on this system');
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${widget.value.url}', trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatPermissions(String v) {
|
||||
var splt = v.split(';');
|
||||
|
||||
if (splt.length == 0) return "None";
|
||||
|
||||
List<String> result = [];
|
||||
|
||||
if (splt.contains("A")) result.add(" - Admin");
|
||||
if (splt.contains("UR")) result.add(" - Read Account");
|
||||
if (splt.contains("CR")) result.add(" - Read Messages");
|
||||
if (splt.contains("CS")) result.add(" - Send Messages");
|
||||
|
||||
return result.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,22 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
@@ -39,9 +46,9 @@ enum EditState { none, editing, saving }
|
||||
enum ChannelViewPageInitState { loading, okay, error }
|
||||
|
||||
class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
late ImmediateFuture<String?> _futureSubscribeKey;
|
||||
late ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions;
|
||||
late ImmediateFuture<UserPreview> _futureOwner;
|
||||
ImmediateFuture<String?> _futureSubscribeKey = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
||||
|
||||
final TextEditingController _ctrlDisplayName = TextEditingController();
|
||||
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
||||
@@ -63,30 +70,36 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initStateAsync();
|
||||
_initStateAsync(true);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void _initStateAsync() async {
|
||||
Future<void> _initStateAsync(bool usePreload) async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (widget.preloadedData != null) {
|
||||
channelPreview = widget.preloadedData!.$1.toPreview();
|
||||
if (widget.preloadedData != null && usePreload) {
|
||||
channel = widget.preloadedData!.$1;
|
||||
subscription = widget.preloadedData!.$2;
|
||||
channelPreview = widget.preloadedData!.$1.toPreview(widget.preloadedData!.$2);
|
||||
} else {
|
||||
try {
|
||||
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
||||
setState(() {
|
||||
channelPreview = p;
|
||||
subscription = p.subscription;
|
||||
});
|
||||
|
||||
if (p.ownerUserID == userAcc.userID) {
|
||||
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
||||
setState(() {
|
||||
channel = r.channel;
|
||||
subscription = r.subscription;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
channel = null;
|
||||
subscription = null; //TODO get own subscription on this channel, even though its foreign channel
|
||||
});
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
||||
@@ -97,6 +110,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
this.loadingState = ChannelViewPageInitState.okay;
|
||||
|
||||
assert(channelPreview != null);
|
||||
@@ -123,6 +137,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -141,7 +156,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
if (loadingState == ChannelViewPageInitState.loading) {
|
||||
child = Center(child: CircularProgressIndicator());
|
||||
} else if (loadingState == ChannelViewPageInitState.error) {
|
||||
child = Center(child: Text('Error: ' + errorMessage)); //TODO better error
|
||||
child = ErrorDisplay(errorMessage: errorMessage);
|
||||
} else if (loadingState == ChannelViewPageInitState.okay && channelPreview!.ownerUserID == userAcc.userID) {
|
||||
child = _buildOwnedChannelView(context, this.channel!);
|
||||
} else {
|
||||
@@ -157,6 +172,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
|
||||
Widget _buildOwnedChannelView(BuildContext context, Channel channel) {
|
||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||
final isSubscribed = (subscription != null && subscription!.confirmed);
|
||||
|
||||
return SingleChildScrollView(
|
||||
@@ -167,12 +183,14 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
children: [
|
||||
_buildQRCode(context),
|
||||
SizedBox(height: 8),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'ChannelID',
|
||||
values: [channel.channelID],
|
||||
),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidInputNumeric,
|
||||
@@ -186,7 +204,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (own)',
|
||||
values: [_formatSubscriptionStatus(this.subscription)],
|
||||
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
||||
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, null, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
),
|
||||
_buildForeignSubscriptions(context),
|
||||
_buildOwnerCard(context, true),
|
||||
@@ -199,6 +218,13 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: channel));
|
||||
},
|
||||
),
|
||||
if (channel.ownerUserID == userAccUserID)
|
||||
UI.button(
|
||||
text: "Delete Channel",
|
||||
onPressed: () {
|
||||
Toaster.info("Not Implemented", "... will be implemented in a later version"); // TODO
|
||||
},
|
||||
color: Colors.red[900]),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -206,7 +232,53 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
|
||||
Widget _buildForeignChannelView(BuildContext context, ChannelPreview channel) {
|
||||
final isSubscribed = (subscription != null && subscription!.confirmed);
|
||||
Widget subCard;
|
||||
|
||||
if (subscription != null && subscription!.confirmed && subscription!.active) {
|
||||
subCard = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (foreign)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, null, _deactivate)],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
);
|
||||
} else if (subscription != null && subscription!.confirmed && !subscription!.active) {
|
||||
subCard = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (foreign)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really (permantenly) delete your subscription to this channel?')), (FontAwesomeIcons.solidSquareRss, null, _activate)],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
);
|
||||
} else if (subscription != null && !subscription!.confirmed) {
|
||||
subCard = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (foreign)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really withdraw your subscription-request to this channel?'))],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
);
|
||||
} else if (subscription == null) {
|
||||
subCard = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (foreign)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
iconActions: [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
);
|
||||
} else {
|
||||
subCard = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (foreign)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
@@ -215,12 +287,14 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'ChannelID',
|
||||
values: [channel.channelID],
|
||||
),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidInputNumeric,
|
||||
@@ -229,15 +303,16 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
),
|
||||
_buildDisplayNameCard(context, false),
|
||||
_buildDescriptionNameCard(context, false),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||
title: 'Subscription (own)',
|
||||
values: [_formatSubscriptionStatus(subscription)],
|
||||
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
||||
),
|
||||
subCard,
|
||||
_buildForeignSubscriptions(context),
|
||||
_buildOwnerCard(context, false),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidEnvelope,
|
||||
title: 'Messages',
|
||||
values: [channel.messagesSent.toString()],
|
||||
mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, alertText: null, filter: MessageFilter(channelIDs: [channel.channelID]))) : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -258,7 +333,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
icon: FontAwesomeIcons.solidDiagramSuccessor,
|
||||
title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')',
|
||||
values: [_formatSubscriptionStatus(sub)],
|
||||
iconActions: _getForeignSubActions(sub),
|
||||
iconActions: _getForeignIncomingSubActions(sub),
|
||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -274,12 +350,20 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
future: _futureOwner.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'Owner',
|
||||
values: [channelPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
|
||||
);
|
||||
else
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'Owner',
|
||||
values: [(snapshot.data?.username ?? channelPreview!.ownerUserID) + (isOwned ? ' (you)' : '')],
|
||||
);
|
||||
} else {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
@@ -296,8 +380,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
return FutureBuilder(
|
||||
future: _futureSubscribeKey.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
var text = 'TODO' + '\n' + channel!.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
|
||||
if (snapshot.hasData) {
|
||||
final text = (snapshot.data == null) ? ScanResult.createChannelQR(channel!) : ScanResult.createChannelSubscribeQR(channel!, snapshot.data!);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
|
||||
@@ -306,7 +390,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
child: QrImageView(
|
||||
data: text,
|
||||
version: QrVersions.auto,
|
||||
size: 300.0,
|
||||
size: 265.0,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
@@ -318,12 +402,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.hasData && snapshot.data == null) {
|
||||
return const SizedBox(
|
||||
width: 300.0,
|
||||
height: 300.0,
|
||||
child: Center(child: Icon(FontAwesomeIcons.solidSnake, size: 64)),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox(
|
||||
width: 300.0,
|
||||
@@ -366,7 +444,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
icon: FontAwesomeIcons.solidInputText,
|
||||
title: 'DisplayName',
|
||||
values: [_displayNameOverride ?? channelPreview!.displayName],
|
||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDisplayName)] : [],
|
||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDisplayName)] : [],
|
||||
);
|
||||
} else if (_editDisplayName == EditState.saving) {
|
||||
return Padding(
|
||||
@@ -422,7 +500,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
icon: FontAwesomeIcons.solidInputPipe,
|
||||
title: 'Description',
|
||||
values: [_descriptionNameOverride ?? channelPreview?.descriptionName ?? ''],
|
||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDescriptionName)] : [],
|
||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDescriptionName)] : [],
|
||||
);
|
||||
} else if (_editDescriptionName == EditState.saving) {
|
||||
return Padding(
|
||||
@@ -447,14 +525,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribe() {
|
||||
//TODO
|
||||
}
|
||||
|
||||
void _unsubscribe() {
|
||||
//TODO
|
||||
}
|
||||
|
||||
void _showEditDisplayName() {
|
||||
setState(() {
|
||||
_ctrlDisplayName.text = _displayNameOverride ?? channelPreview?.displayName ?? '';
|
||||
@@ -481,6 +551,9 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
});
|
||||
|
||||
widget.needsReload?.call();
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to save DisplayName');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to save DisplayName');
|
||||
@@ -513,31 +586,174 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
});
|
||||
|
||||
widget.needsReload?.call();
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to save description');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to save DescriptionName: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to save DescriptionName');
|
||||
Toaster.error("Error", 'Failed to save description');
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelForeignSubscription(Subscription sub) {
|
||||
//TODO
|
||||
void _subscribe() async {
|
||||
final acc = AppAuth();
|
||||
|
||||
try {
|
||||
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channelID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
if (sub.confirmed) {
|
||||
Toaster.success("Success", 'Subscribed to channel');
|
||||
} else {
|
||||
Toaster.success("Success", 'Requested subscription to channel');
|
||||
}
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmForeignSubscription(Subscription sub) {
|
||||
//TODO
|
||||
void _unsubscribe({String? confirm = null}) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (subscription == null) return;
|
||||
|
||||
if (confirm != null) {
|
||||
final r = await UIDialogs.showConfirmDialog(context, confirm, okText: 'Unsubscribe', cancelText: 'Cancel');
|
||||
if (!r) return;
|
||||
}
|
||||
|
||||
void _denyForeignSubscription(Subscription sub) {
|
||||
//TODO
|
||||
try {
|
||||
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Unsubscribed from channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _deactivate() async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (subscription == null) return;
|
||||
|
||||
try {
|
||||
await APIClient.deactivateSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Unsubscribed from channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _activate() async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (subscription == null) return;
|
||||
|
||||
try {
|
||||
await APIClient.activateSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Subscribed to channel');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelForeignSubscription(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
try {
|
||||
await APIClient.unconfirmSubscription(acc, widget.channelID, sub.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Subscription succesfully revoked');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to revoke subscription');
|
||||
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to revoke subscription');
|
||||
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmForeignSubscription(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
try {
|
||||
await APIClient.confirmSubscription(acc, widget.channelID, sub.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Subscription succesfully confirmed');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to confirm subscription');
|
||||
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to confirm subscription');
|
||||
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _denyForeignSubscription(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
try {
|
||||
await APIClient.deleteSubscription(acc, widget.channelID, sub.subscriptionID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
await _initStateAsync(false);
|
||||
|
||||
Toaster.success("Success", 'Subscription request succesfully denied');
|
||||
} on APIException catch (exc, trace) {
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to deny subscription');
|
||||
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to deny subscription');
|
||||
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatSubscriptionStatus(Subscription? subscription) {
|
||||
if (subscription == null) {
|
||||
return 'Not Subscribed';
|
||||
} else if (subscription.confirmed) {
|
||||
return 'Subscribed';
|
||||
} else {
|
||||
} else if (subscription.confirmed && subscription.active) {
|
||||
return 'Subscribed & Active';
|
||||
} else if (subscription.confirmed && !subscription.active) {
|
||||
return 'Subscribed & Inactive';
|
||||
} else if (!subscription.confirmed) {
|
||||
return 'Requested';
|
||||
} else {
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,13 +807,13 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
List<(IconData, void Function())> _getForeignSubActions(Subscription sub) {
|
||||
List<(IconData, Color?, void Function())> _getForeignIncomingSubActions(Subscription sub) {
|
||||
if (sub.confirmed) {
|
||||
return [(FontAwesomeIcons.solidSquareXmark, () => _cancelForeignSubscription(sub))];
|
||||
return [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _cancelForeignSubscription(sub))];
|
||||
} else {
|
||||
return [
|
||||
(FontAwesomeIcons.solidSquareCheck, () => _confirmForeignSubscription(sub)),
|
||||
(FontAwesomeIcons.solidSquareXmark, () => _denyForeignSubscription(sub)),
|
||||
(FontAwesomeIcons.solidSquareCheck, Colors.green[900], () => _confirmForeignSubscription(sub)),
|
||||
(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _denyForeignSubscription(sub)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
107
flutter/lib/pages/client_list/client_list.dart
Normal file
107
flutter/lib/pages/client_list/client_list.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart';
|
||||
|
||||
class ClientListPage extends StatefulWidget {
|
||||
const ClientListPage({super.key});
|
||||
|
||||
@override
|
||||
State<ClientListPage> createState() => _ClientListPageState();
|
||||
}
|
||||
|
||||
class _ClientListPageState extends State<ClientListPage> {
|
||||
final PagingController<int, Client> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('ClientListPage::dispose');
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start ClientListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = (await APIClient.getClientList(acc)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list clients: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Clients",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: "All clients connected with this account",
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildList(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<int, Client>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Client>(
|
||||
itemBuilder: (context, item, index) => ClientListItem(item: item),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
flutter/lib/pages/client_list/client_list_item.dart
Normal file
79
flutter/lib/pages/client_list/client_list_item.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
|
||||
enum ClientListItemMode {
|
||||
Messages,
|
||||
Extended,
|
||||
}
|
||||
|
||||
class ClientListItem extends StatelessWidget {
|
||||
const ClientListItem({
|
||||
required this.item,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Client item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildIcon(context),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name ?? item.clientID,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dateFormat.format(DateTime.parse(item.timestampCreated).toLocal()),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(item.agentModel.toString() + " " + item.agentVersion.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(BuildContext context) {
|
||||
if (item.type == "ANDROID") return Icon(FontAwesomeIcons.android, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
if (item.type == "IOS") return Icon(FontAwesomeIcons.apple, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
if (item.type == "LINUX") return Icon(FontAwesomeIcons.linux, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
if (item.type == "MACOS") return Icon(FontAwesomeIcons.appleWhole, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
if (item.type == "WINDOWS") return Icon(FontAwesomeIcons.windows, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
|
||||
return Icon(FontAwesomeIcons.solidSignature, color: Theme.of(context).colorScheme.outline, size: 32);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/notifier.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
@@ -48,6 +54,12 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
||||
text: 'Show Simple Notification',
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: _copyToken,
|
||||
text: 'Query+Copy FCM Token',
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: _sendTokenToServer,
|
||||
@@ -56,8 +68,32 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
||||
SizedBox(height: 20),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null),
|
||||
text: 'Show local notification',
|
||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null),
|
||||
text: 'Show local notification (generic)',
|
||||
),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 0),
|
||||
text: 'Show local notification (Prio = 0)',
|
||||
),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 1),
|
||||
text: 'Show local notification (Prio = 1)',
|
||||
),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 2),
|
||||
text: 'Show local notification (Prio = 2)',
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
UI.button(
|
||||
big: false,
|
||||
onPressed: () {
|
||||
AppSettings().update((p) => p.reset());
|
||||
Toaster.success("Success", "AppSettings reset to default");
|
||||
},
|
||||
text: 'Reset AppSettings to default',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -66,7 +102,46 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendTokenToServer() {
|
||||
//TODO
|
||||
void _sendTokenToServer() async {
|
||||
try {
|
||||
final auth = AppAuth();
|
||||
|
||||
final clientID = auth.getClientID();
|
||||
if (clientID == null) {
|
||||
Toaster.error("Error", "No Client set");
|
||||
return;
|
||||
}
|
||||
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken == null) {
|
||||
Toaster.error("Error", "No FCM token returned from Firebase");
|
||||
return;
|
||||
}
|
||||
|
||||
var newClient = await APIClient.updateClient(auth, clientID, fcmToken: fcmToken);
|
||||
auth.setClientAndClientID(newClient);
|
||||
|
||||
Toaster.success("Success", "Token sent to server");
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", "An error occurred while sending the token: ${exc.toString()}");
|
||||
ApplicationLog.error("An error occurred while sending the token: ${exc.toString()}", trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _copyToken() async {
|
||||
try {
|
||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
if (fcmToken == null) {
|
||||
Toaster.error("Error", "No FCM token returned from Firebase");
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.setData(new ClipboardData(text: fcmToken));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${fcmToken}\n================= [/CLIPBOARD] =================');
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", "An error occurred while sending the token: ${exc.toString()}");
|
||||
ApplicationLog.error("An error occurred while sending the token: ${exc.toString()}", trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,11 @@ class _DebugColorsPageState extends State<DebugColorsPage> {
|
||||
buildCol("colorScheme.surface", Theme.of(context).colorScheme.surface),
|
||||
buildCol("colorScheme.onSurface", Theme.of(context).colorScheme.onSurface),
|
||||
buildCol("colorScheme.surfaceTint", Theme.of(context).colorScheme.surfaceTint),
|
||||
buildCol("colorScheme.surfaceVariant", Theme.of(context).colorScheme.surfaceVariant),
|
||||
buildCol("colorScheme.surfaceVariant", Theme.of(context).colorScheme.surfaceContainerHighest),
|
||||
buildCol("colorScheme.inverseSurface", Theme.of(context).colorScheme.inverseSurface),
|
||||
buildCol("colorScheme.onInverseSurface", Theme.of(context).colorScheme.onInverseSurface),
|
||||
buildCol("colorScheme.background", Theme.of(context).colorScheme.background),
|
||||
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onBackground),
|
||||
buildCol("colorScheme.background", Theme.of(context).colorScheme.surface),
|
||||
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onSurface),
|
||||
buildCol("colorScheme.error", Theme.of(context).colorScheme.error),
|
||||
buildCol("colorScheme.onError", Theme.of(context).colorScheme.onError),
|
||||
buildCol("colorScheme.errorContainer", Theme.of(context).colorScheme.errorContainer),
|
||||
@@ -98,7 +98,7 @@ class _DebugColorsPageState extends State<DebugColorsPage> {
|
||||
buildCol("badgeTheme.backgroundColor", Theme.of(context).badgeTheme.backgroundColor),
|
||||
buildCol("bannerTheme.backgroundColor", Theme.of(context).bannerTheme.backgroundColor),
|
||||
buildCol("bottomAppBarTheme.color", Theme.of(context).bottomAppBarTheme.color),
|
||||
buildCol("buttonTheme.colorScheme.background", Theme.of(context).buttonTheme.colorScheme?.background),
|
||||
buildCol("buttonTheme.colorScheme.background", Theme.of(context).buttonTheme.colorScheme?.surface),
|
||||
buildCol("buttonTheme.colorScheme.primary", Theme.of(context).buttonTheme.colorScheme?.primary),
|
||||
buildCol("buttonTheme.colorScheme.secondary", Theme.of(context).buttonTheme.colorScheme?.secondary),
|
||||
buildCol("cardTheme.color", Theme.of(context).cardTheme.color),
|
||||
|
||||
@@ -11,7 +11,7 @@ class DebugLogsPage extends StatefulWidget {
|
||||
class _DebugLogsPageState extends State<DebugLogsPage> {
|
||||
Box<SCNLog> logBox = Hive.box<SCNLog>('scn-logs');
|
||||
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -23,7 +23,7 @@ class _DebugMainPageState extends State<DebugMainPage> {
|
||||
DebugMainPageSubPage.actions: DebugActionsPage(),
|
||||
};
|
||||
|
||||
DebugMainPageSubPage _subPage = DebugMainPageSubPage.colors;
|
||||
DebugMainPageSubPage _subPage = DebugMainPageSubPage.logs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -53,14 +53,14 @@ class _DebugMainPageState extends State<DebugMainPage> {
|
||||
return SegmentedButton<DebugMainPageSubPage>(
|
||||
showSelectedIcon: false,
|
||||
segments: const <ButtonSegment<DebugMainPageSubPage>>[
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.colors, icon: Icon(FontAwesomeIcons.solidPaintRoller, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.actions, icon: Icon(FontAwesomeIcons.solidHammer, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.requests, icon: Icon(FontAwesomeIcons.solidNetworkWired, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.persistence, icon: Icon(FontAwesomeIcons.solidDatabase, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.colors, icon: Icon(FontAwesomeIcons.solidPaintRoller, size: 14)),
|
||||
],
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
|
||||
visualDensity: VisualDensity(horizontal: -3, vertical: -3),
|
||||
),
|
||||
selected: <DebugMainPageSubPage>{_subPage},
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogs.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class DebugPersistencePage extends StatefulWidget {
|
||||
@override
|
||||
@@ -39,6 +44,7 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
|
||||
_buildHiveCard(context, () => Hive.box<SCNMessage>('scn-message-cache'), 'scn-message-cache'),
|
||||
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
|
||||
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
|
||||
_buildFailureLogCard(context, Globals().rawFailureLogsDir),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -85,4 +91,25 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFailureLogCard(BuildContext context, Directory dir) {
|
||||
return Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => DebugFailureLogsPage(dir: dir.path));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Failure [/${path.basename(dir.path)}/]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 40, child: Text("${dir.listSync().length}", textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
|
||||
class DebugFailureLogFilePage extends StatefulWidget {
|
||||
final String path;
|
||||
|
||||
DebugFailureLogFilePage({required this.path}) {}
|
||||
|
||||
@override
|
||||
State<DebugFailureLogFilePage> createState() => _DebugFailureLogFilePageState();
|
||||
}
|
||||
|
||||
class _DebugFailureLogFilePageState extends State<DebugFailureLogFilePage> {
|
||||
ImmediateFuture<String>? _futureContent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureContent = ImmediateFuture.ofFuture(new File(this.widget.path).readAsString());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: 'FailureLog',
|
||||
showSearch: false,
|
||||
child: () {
|
||||
if (_futureContent == null) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return FutureBuilder(
|
||||
future: _futureContent!.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (_futureContent?.value != null) {
|
||||
return _buildContent(context, _futureContent!.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return _buildContent(context, snapshot.data!);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, String value) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(value, style: TextStyle(fontFamily: "monospace")),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
86
flutter/lib/pages/debug/debug_persistence_failurelogs.dart
Normal file
86
flutter/lib/pages/debug/debug_persistence_failurelogs.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogfile.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
class DebugFailureLogsPage extends StatefulWidget {
|
||||
final String dir;
|
||||
|
||||
DebugFailureLogsPage({required this.dir});
|
||||
|
||||
@override
|
||||
State<DebugFailureLogsPage> createState() => _DebugFailureLogsPageState();
|
||||
}
|
||||
|
||||
class _DebugFailureLogsPageState extends State<DebugFailureLogsPage> {
|
||||
List<String> files = [];
|
||||
|
||||
_DebugFailureLogsPageState() {
|
||||
files = _listFilesInRawLogFolder();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: 'F-Logs',
|
||||
showSearch: false,
|
||||
child: ListView.separated(
|
||||
itemCount: files.length,
|
||||
itemBuilder: (context, listIndex) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => DebugFailureLogFilePage(path: files[listIndex]));
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(path.basename(files[listIndex]), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
|
||||
IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.trash),
|
||||
tooltip: 'Delete',
|
||||
iconSize: 16,
|
||||
color: Colors.red,
|
||||
onPressed: () => _deleteFile(context, files[listIndex]),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Divider(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _listFilesInRawLogFolder() {
|
||||
final fse = Globals().rawFailureLogsDir.listSync();
|
||||
|
||||
ApplicationLog.debug("Found ${fse.length} files in raw log folder '${Globals().rawFailureLogsDir.path}'");
|
||||
|
||||
var paths = fse.where((element) => element is File).map((e) => e.path).toList();
|
||||
|
||||
paths.sort((a, b) => -1 * a.compareTo(b));
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
void _deleteFile(BuildContext context, String fil) {
|
||||
final file = File(fil);
|
||||
|
||||
file.deleteSync();
|
||||
|
||||
setState(() {
|
||||
files = _listFilesInRawLogFolder();
|
||||
});
|
||||
|
||||
Toaster.info("Okay", "File deleted");
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ class DebugSharedPrefPage extends StatelessWidget {
|
||||
final SharedPreferences sharedPref;
|
||||
final List<String> keys;
|
||||
|
||||
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList();
|
||||
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList() {
|
||||
keys.sort((a, b) => a.compareTo(b));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
@@ -6,11 +8,19 @@ import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class DebugRequestViewPage extends StatelessWidget {
|
||||
class DebugRequestViewPage extends StatefulWidget {
|
||||
final SCNRequest request;
|
||||
|
||||
DebugRequestViewPage({required this.request});
|
||||
|
||||
@override
|
||||
State<DebugRequestViewPage> createState() => _DebugRequestViewPageState();
|
||||
}
|
||||
|
||||
class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
||||
Set<String> _monospaceMode = new Set();
|
||||
Set<String> _prettyJson = new Set();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
@@ -23,22 +33,23 @@ class DebugRequestViewPage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
...buildRow(context, "Name", request.name),
|
||||
...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()),
|
||||
...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()),
|
||||
...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()),
|
||||
...buildRow(context, "name", "Name", widget.request.name),
|
||||
...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()),
|
||||
...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()),
|
||||
...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()),
|
||||
Divider(),
|
||||
...buildRow(context, "Method", request.method),
|
||||
...buildRow(context, "URL", request.url),
|
||||
if (request.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
||||
if (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody),
|
||||
...buildRow(context, "method", "Method", widget.request.method),
|
||||
...buildRow(context, "url", "URL", widget.request.url, mono: true),
|
||||
if (widget.request.requestHeaders.isNotEmpty) ...buildRow(context, "request_headers", "Request->Headers", widget.request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true),
|
||||
if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true),
|
||||
UI.button(text: "Copy request as curl", onPressed: _copyCurl, tonal: true),
|
||||
Divider(),
|
||||
if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()),
|
||||
if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody),
|
||||
if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
||||
if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()),
|
||||
if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true),
|
||||
if (widget.request.responseHeaders.isNotEmpty) ...buildRow(context, "response_headers", "Reponse->Headers", widget.request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true, json: false),
|
||||
Divider(),
|
||||
if (request.error != '') ...buildRow(context, "Error", request.error),
|
||||
if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace),
|
||||
if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true),
|
||||
if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -46,7 +57,19 @@ class DebugRequestViewPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildRow(BuildContext context, String title, String value) {
|
||||
List<Widget> buildRow(BuildContext context, String key, String title, String value, {bool? json, bool? mono}) {
|
||||
var isMono = _monospaceMode.contains(key);
|
||||
var isJson = _prettyJson.contains(key);
|
||||
|
||||
if (isJson) {
|
||||
try {
|
||||
var jsonValue = jsonDecode(value);
|
||||
value = JsonEncoder.withIndent(' ').convert(jsonValue);
|
||||
} catch (e) {
|
||||
value = ('Error parsing JSON: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0),
|
||||
@@ -58,27 +81,68 @@ class DebugRequestViewPage extends StatelessWidget {
|
||||
UI.buttonIconOnly(
|
||||
iconSize: 14,
|
||||
onPressed: () {
|
||||
Clipboard.setData(new ClipboardData(text: title));
|
||||
Clipboard.setData(new ClipboardData(text: value));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${title}\n================= [/CLIPBOARD] =================');
|
||||
print('================= [CLIPBOARD] =================\n${value}\n================= [/CLIPBOARD] =================');
|
||||
},
|
||||
icon: FontAwesomeIcons.copy,
|
||||
),
|
||||
if (mono == true)
|
||||
UI.buttonIconOnly(
|
||||
icon: isMono ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_monospaceMode.contains(key) ? _monospaceMode.remove(key) : _monospaceMode.add(key);
|
||||
});
|
||||
},
|
||||
),
|
||||
if (json == true)
|
||||
UI.buttonIconOnly(
|
||||
icon: isJson ? FontAwesomeIcons.bracketsRound : FontAwesomeIcons.bracketsCurly,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_prettyJson.contains(key) ? _prettyJson.remove(key) : _prettyJson.add(key);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Card.filled(
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
|
||||
color: widget.request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
|
||||
child: (isMono || isJson)
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SelectableText(
|
||||
value,
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
),
|
||||
)
|
||||
: SelectableText(
|
||||
value,
|
||||
minLines: 1,
|
||||
maxLines: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _copyCurl() {
|
||||
final method = '-X ${widget.request.method}';
|
||||
final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' ');
|
||||
final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : '';
|
||||
|
||||
final curlParts = ['curl', method, header, '"${widget.request.url}"', body];
|
||||
|
||||
final txt = curlParts.where((part) => part.isNotEmpty).join(' ');
|
||||
|
||||
Clipboard.setData(new ClipboardData(text: txt));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${txt}\n================= [/CLIPBOARD] =================');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class DebugRequestsPage extends StatefulWidget {
|
||||
class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests');
|
||||
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -47,10 +47,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
title: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
@@ -61,7 +57,14 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(req.type),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
req.error,
|
||||
maxLines: 1,
|
||||
@@ -81,10 +84,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
@@ -92,7 +91,13 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
subtitle: Text(req.type),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(req.type),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FilteredMessageViewPage extends StatefulWidget {
|
||||
const FilteredMessageViewPage({
|
||||
required this.title,
|
||||
required this.filter,
|
||||
required this.alertText,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final MessageFilter filter;
|
||||
final String? alertText;
|
||||
|
||||
@override
|
||||
State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState();
|
||||
}
|
||||
|
||||
class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
|
||||
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
||||
|
||||
Map<String, Channel>? _channels = null;
|
||||
bool _channelsFetched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_channels = SCNDataCache().getChannelMap();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(String thisPageToken) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
final cfg = Provider.of<AppSettings>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start FilteredMessageViewPage::_pagingController::_fetchPage [ ${thisPageToken} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (_channels == null || !_channelsFetched) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
setState(() {
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
_channelsFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter, includeNonSuscribed: true);
|
||||
|
||||
SCNDataCache().addToMessageCache(newItems); // no await
|
||||
|
||||
if (npt == '@end') {
|
||||
_pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
_pagingController.appendPage(newItems, npt);
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list channel-messages: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = _buildMessageList(context);
|
||||
|
||||
if (widget.alertText != null) {
|
||||
child = Column(
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: widget.alertText!,
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
Expanded(
|
||||
child: child,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SCNScaffold(
|
||||
title: this.widget.title,
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<String, SCNMessage>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
|
||||
itemBuilder: (context, item, index) => MessageListItem(
|
||||
message: item,
|
||||
allChannels: _channels ?? {},
|
||||
onPressed: () {
|
||||
Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal file
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal file
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
class KeyTokenCreateDialog extends StatefulWidget {
|
||||
final void Function(KeyToken, String) onCreated;
|
||||
|
||||
const KeyTokenCreateDialog({
|
||||
required this.onCreated,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_KeyTokenCreateDialogState createState() => _KeyTokenCreateDialogState();
|
||||
}
|
||||
|
||||
class _KeyTokenCreateDialogState extends State<KeyTokenCreateDialog> {
|
||||
TextEditingController _ctrlName = TextEditingController();
|
||||
Set<String> selectedPermissions = {'CS'};
|
||||
|
||||
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
||||
|
||||
bool allChannels = true;
|
||||
Set<String> selectedChannels = new Set<String>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
setState(() {
|
||||
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrlName.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Create new key'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 400,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildNameCtrl(context),
|
||||
SizedBox(height: 32),
|
||||
_buildPermissionCtrl(context),
|
||||
SizedBox(height: 32),
|
||||
_buildChannelCtrl(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Create'),
|
||||
onPressed: _create,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameCtrl(BuildContext context) {
|
||||
return TextField(
|
||||
controller: _ctrlName,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Key name',
|
||||
hintText: 'Enter a name for the new key',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPermissionCtrl(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('Permissions:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemBuilder: (builder, index) {
|
||||
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
||||
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text(txt),
|
||||
leading: Icon(
|
||||
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selectedPermissions.contains(prm)) {
|
||||
selectedPermissions.remove(prm);
|
||||
} else {
|
||||
selectedPermissions.add(prm);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: 4,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChannelCtrl(BuildContext context) {
|
||||
return FutureBuilder<List<Channel>>(
|
||||
future: _futureOwnedChannels.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
}
|
||||
|
||||
final ownChannels = snapshot.data!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('Channels:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text('All Channels'),
|
||||
leading: Icon(
|
||||
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
allChannels = !allChannels;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
if (!allChannels)
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemBuilder: (builder, index) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text(ownChannels[index].displayName),
|
||||
leading: Icon(
|
||||
selectedChannels.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selectedChannels.contains(ownChannels[index].channelID)) {
|
||||
selectedChannels.remove(ownChannels[index].channelID);
|
||||
} else {
|
||||
selectedChannels.add(ownChannels[index].channelID);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: ownChannels.length,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _create() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (!userAcc.isAuth()) return;
|
||||
|
||||
if (_ctrlName.text.isEmpty) {
|
||||
Toaster.error('Missing data', 'Please enter a name for the key');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final perm = selectedPermissions.join(';');
|
||||
final channels = allChannels ? <String>[] : selectedChannels.toList();
|
||||
|
||||
var kt = await APIClient.createKeyToken(userAcc, _ctrlName.text, perm, allChannels, channels: channels);
|
||||
Toaster.success('Success', 'Key created successfully');
|
||||
Navigator.of(context).pop();
|
||||
widget.onCreated(kt.keyToken, kt.token);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to create keytoken: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to create key: ${exc.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
109
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal file
109
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class KeyTokenCreatedModal extends StatelessWidget {
|
||||
final KeyToken keytoken;
|
||||
final String tokenValue;
|
||||
|
||||
const KeyTokenCreatedModal({
|
||||
Key? key,
|
||||
required this.keytoken,
|
||||
required this.tokenValue,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final acc = AppAuth();
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('A new key was created'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 350,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const BadgeDisplay(
|
||||
text: "Please copy and save the token now, it cannot be retrieved later.",
|
||||
icon: null,
|
||||
mode: BadgeMode.warn,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (acc.userID != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'UserID',
|
||||
values: [acc.userID!],
|
||||
iconActions: [(FontAwesomeIcons.copy, null, () => _copy(acc.userID!))],
|
||||
),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'KeyTokenID',
|
||||
values: [keytoken.keytokenID],
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidInputText,
|
||||
title: 'Name',
|
||||
values: [keytoken.name],
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||
title: 'Permissions',
|
||||
values: _formatPermissions(keytoken.permissions),
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidKey,
|
||||
title: 'Token',
|
||||
values: [tokenValue],
|
||||
iconActions: [(FontAwesomeIcons.copy, null, () => _copy(tokenValue))],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Close'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _formatPermissions(String v) {
|
||||
var splt = v.split(';');
|
||||
|
||||
if (splt.length == 0) return ["None"];
|
||||
|
||||
List<String> result = [];
|
||||
|
||||
if (splt.contains("A")) result.add("Admin");
|
||||
if (splt.contains("UR")) result.add("Read Account");
|
||||
if (splt.contains("CR")) result.add("Read Messages");
|
||||
if (splt.contains("CS")) result.add("Send Messages");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _copy(String v) {
|
||||
Clipboard.setData(new ClipboardData(text: v));
|
||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
|
||||
}
|
||||
}
|
||||
136
flutter/lib/pages/keytoken_list/keytoken_list.dart
Normal file
136
flutter/lib/pages/keytoken_list/keytoken_list.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
|
||||
|
||||
class KeyTokenListPage extends StatefulWidget {
|
||||
const KeyTokenListPage({super.key});
|
||||
|
||||
@override
|
||||
State<KeyTokenListPage> createState() => _KeyTokenListPageState();
|
||||
}
|
||||
|
||||
class _KeyTokenListPageState extends State<KeyTokenListPage> {
|
||||
final PagingController<int, KeyToken> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('KeyTokenListPage::dispose');
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start KeyTokenListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = (await APIClient.getKeyTokenList(acc)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list keytokens: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Keys",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: "These are your keys.\nKeys can be used to send messages and access your account.\n\nKeys can have different sets of permissions, the Admin-Key has full-access to your account",
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildList(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
heroTag: 'fab_keytokenlist_plus',
|
||||
onPressed: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => KeyTokenCreateDialog(onCreated: _created),
|
||||
);
|
||||
},
|
||||
child: const Icon(FontAwesomeIcons.plus),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<int, KeyToken>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
|
||||
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _created(KeyToken token, String tokValue) {
|
||||
setState(() {
|
||||
_pagingController.itemList?.insert(0, token);
|
||||
});
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => KeyTokenCreatedModal(keytoken: token, tokenValue: tokValue),
|
||||
);
|
||||
}
|
||||
|
||||
void _fullRefresh() {
|
||||
ApplicationLog.debug('KeytokenListPage::fullRefresh');
|
||||
_pagingController.refresh();
|
||||
}
|
||||
}
|
||||
116
flutter/lib/pages/keytoken_list/keytoken_list_item.dart
Normal file
116
flutter/lib/pages/keytoken_list/keytoken_list_item.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
enum KeyTokenListItemMode {
|
||||
Messages,
|
||||
Extended,
|
||||
}
|
||||
|
||||
class KeyTokenListItem extends StatelessWidget {
|
||||
const KeyTokenListItem({
|
||||
required this.item,
|
||||
required this.needsReload,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final KeyToken item;
|
||||
final void Function()? needsReload;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navi.push(context, () => KeyTokenViewPage(keytokenID: item.keytokenID, preloadedData: item, needsReload: needsReload));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidGearCode, color: Theme.of(context).colorScheme.outline, size: 32),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(item.timestampLastUsed == null) ? '' : dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Permissions: " + _formatPermissions(item.permissions, item.allChannels, item.channels),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
),
|
||||
),
|
||||
Text(item.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent with the key \'${item.name}\'', filter: MessageFilter(usedKeys: [item.keytokenID])));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPermissions(String v, bool allChannels, List<String> channels) {
|
||||
var splt = v.split(';');
|
||||
|
||||
if (splt.length == 0) return "None";
|
||||
|
||||
var a = splt.contains("A");
|
||||
var ur = splt.contains("UR");
|
||||
var cr = splt.contains("CR");
|
||||
var cs = splt.contains("CS");
|
||||
|
||||
if (a) return "Admin";
|
||||
if (cr && cs && allChannels) return "Read+Send";
|
||||
if (cr && cs && !allChannels) return "Read+Send (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
||||
if (ur && !cr && !cs) return "Account-Read";
|
||||
if (cr && !cs && !allChannels) return "Read-only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
||||
if (cr && !cs && allChannels) return "Read-only";
|
||||
if (cs && !allChannels) return "Send-Only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
||||
if (cs && allChannels) return "Send-Only";
|
||||
|
||||
return "{ " + v + " | " + (allChannels ? 'all' : '${channels.length}') + " }";
|
||||
}
|
||||
}
|
||||
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal file
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
|
||||
class EditKeyTokenChannelsDialog extends StatefulWidget {
|
||||
final List<Channel> ownedChannels;
|
||||
final KeyTokenPreview keytoken;
|
||||
|
||||
final void Function(Set<String>) onUpdateChannels;
|
||||
final void Function() onUpdateSetAllChannels;
|
||||
|
||||
const EditKeyTokenChannelsDialog({
|
||||
required this.ownedChannels,
|
||||
required this.keytoken,
|
||||
required this.onUpdateChannels,
|
||||
required this.onUpdateSetAllChannels,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_EditKeyTokenChannelsDialogState createState() => _EditKeyTokenChannelsDialogState();
|
||||
}
|
||||
|
||||
class _EditKeyTokenChannelsDialogState extends State<EditKeyTokenChannelsDialog> {
|
||||
late bool allChannels;
|
||||
late Set<String> selectedEntries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
allChannels = widget.keytoken.allChannels;
|
||||
selectedEntries = (widget.keytoken.channels).toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var ownChannels = widget.ownedChannels.toList();
|
||||
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Channels'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text('All Channels'),
|
||||
leading: Icon(
|
||||
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
allChannels = !allChannels;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
if (!allChannels)
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (builder, index) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text(ownChannels[index].displayName),
|
||||
leading: Icon(
|
||||
selectedEntries.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selectedEntries.contains(ownChannels[index].channelID)) {
|
||||
selectedEntries.remove(ownChannels[index].channelID);
|
||||
} else {
|
||||
selectedEntries.add(ownChannels[index].channelID);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: ownChannels.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Update'),
|
||||
onPressed: () {
|
||||
if (allChannels) {
|
||||
widget.onUpdateSetAllChannels();
|
||||
} else {
|
||||
widget.onUpdateChannels(selectedEntries);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
|
||||
class EditKeyTokenPermissionsDialog extends StatefulWidget {
|
||||
final KeyTokenPreview keytoken;
|
||||
|
||||
final void Function(String) onUpdatePermissions;
|
||||
|
||||
const EditKeyTokenPermissionsDialog({
|
||||
required this.keytoken,
|
||||
required this.onUpdatePermissions,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_EditKeyTokenPermissionsDialogState createState() => _EditKeyTokenPermissionsDialogState();
|
||||
}
|
||||
|
||||
class _EditKeyTokenPermissionsDialogState extends State<EditKeyTokenPermissionsDialog> {
|
||||
Set<String> selectedPermissions = new Set<String>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var p in widget.keytoken.permissions.split(';')) {
|
||||
if (p.isNotEmpty) selectedPermissions.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Permissions'),
|
||||
content: Container(
|
||||
width: 9000,
|
||||
height: 400,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (builder, index) {
|
||||
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
||||
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||
title: Text(txt),
|
||||
leading: Icon(
|
||||
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selectedPermissions.contains(prm)) {
|
||||
selectedPermissions.remove(prm);
|
||||
} else {
|
||||
selectedPermissions.add(prm);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: 4,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||
child: const Text('Update'),
|
||||
onPressed: () {
|
||||
widget.onUpdatePermissions(selectedPermissions.join(';'));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
634
flutter/lib/pages/keytoken_view/keytoken_view.dart
Normal file
634
flutter/lib/pages/keytoken_view/keytoken_view.dart
Normal file
@@ -0,0 +1,634 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_channel_modal.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_permission_modal.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class KeyTokenViewPage extends StatefulWidget {
|
||||
const KeyTokenViewPage({
|
||||
required this.keytokenID,
|
||||
required this.preloadedData,
|
||||
required this.needsReload,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String keytokenID;
|
||||
final KeyToken? preloadedData;
|
||||
|
||||
final void Function()? needsReload;
|
||||
|
||||
@override
|
||||
State<KeyTokenViewPage> createState() => _KeyTokenViewPageState();
|
||||
}
|
||||
|
||||
enum EditState { none, editing, saving }
|
||||
|
||||
enum KeyTokenViewPageInitState { loading, okay, error }
|
||||
|
||||
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
||||
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
||||
|
||||
ImmediateFuture<Map<String, ChannelPreview>> _futureAllChannels = ImmediateFuture.ofPending();
|
||||
|
||||
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
||||
|
||||
final TextEditingController _ctrlName = TextEditingController();
|
||||
|
||||
int _loadingIndeterminateCounter = 0;
|
||||
|
||||
EditState _editName = EditState.none;
|
||||
String? _nameOverride = null;
|
||||
|
||||
KeyTokenPreview? keytokenPreview;
|
||||
KeyToken? keytoken;
|
||||
|
||||
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
|
||||
String errorMessage = '';
|
||||
|
||||
KeyToken? keytokenUserAccAdmin;
|
||||
KeyToken? keytokenUserAccSend;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initStateAsync(true);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _initStateAsync(bool usePreload) async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (widget.preloadedData != null && usePreload) {
|
||||
keytoken = widget.preloadedData!;
|
||||
keytokenPreview = widget.preloadedData!.toPreview();
|
||||
} else {
|
||||
try {
|
||||
var p = await APIClient.getKeyTokenPreviewByID(userAcc, widget.keytokenID);
|
||||
setState(() {
|
||||
keytokenPreview = p;
|
||||
});
|
||||
|
||||
if (p.ownerUserID == userAcc.userID) {
|
||||
var r = await APIClient.getKeyToken(userAcc, widget.keytokenID);
|
||||
setState(() {
|
||||
keytoken = r;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
keytoken = null;
|
||||
});
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to load data');
|
||||
this.errorMessage = 'Failed to load data: ' + exc.toString();
|
||||
this.loadingState = KeyTokenViewPageInitState.error;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
this.loadingState = KeyTokenViewPageInitState.okay;
|
||||
|
||||
assert(keytokenPreview != null);
|
||||
|
||||
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
|
||||
var cacheUser = userAcc.getUserOrNull();
|
||||
if (cacheUser != null) {
|
||||
_futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview());
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc));
|
||||
}
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID));
|
||||
}
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_futureAllChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.allAny).then((lst) async {
|
||||
Map<String, ChannelPreview> result = {};
|
||||
|
||||
for (var c in lst) result[c.channel.channelID] = c.channel.toPreview(c.subscription);
|
||||
|
||||
if (keytokenPreview != null) {
|
||||
for (var cid in keytokenPreview!.channels) {
|
||||
if (!result.containsKey(cid)) {
|
||||
result[cid] = await APIClient.getChannelPreview(userAcc, cid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}));
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
||||
});
|
||||
|
||||
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenAdmin!).then((token) {
|
||||
setState(() {
|
||||
keytokenUserAccAdmin = token;
|
||||
});
|
||||
});
|
||||
|
||||
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenSend!).then((token) {
|
||||
setState(() {
|
||||
keytokenUserAccSend = token;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrlName.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
var title = "Key";
|
||||
|
||||
Widget child;
|
||||
|
||||
if (loadingState == KeyTokenViewPageInitState.loading) {
|
||||
child = Center(child: CircularProgressIndicator());
|
||||
} else if (loadingState == KeyTokenViewPageInitState.error) {
|
||||
child = ErrorDisplay(errorMessage: errorMessage);
|
||||
} else if (loadingState == KeyTokenViewPageInitState.okay && keytokenPreview!.ownerUserID == userAcc.userID) {
|
||||
child = _buildOwnedKeyTokenView(context, this.keytoken!);
|
||||
title = this.keytoken!.name;
|
||||
} else {
|
||||
child = _buildForeignKeyTokenView(context, this.keytokenPreview!);
|
||||
title = keytokenPreview!.name;
|
||||
}
|
||||
|
||||
return SCNScaffold(
|
||||
title: title,
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'KeyTokenID',
|
||||
values: [
|
||||
keytoken.keytokenID,
|
||||
if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)',
|
||||
if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)',
|
||||
],
|
||||
),
|
||||
_buildNameCard(context, true),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidClock,
|
||||
title: 'Created',
|
||||
values: [dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidClockTwo,
|
||||
title: 'Last Used',
|
||||
values: [(keytoken.timestampLastUsed == null) ? 'Never' : dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
|
||||
),
|
||||
_buildOwnerCard(context, true),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidEnvelope,
|
||||
title: 'Messages',
|
||||
values: [keytoken.messagesSent.toString()],
|
||||
mainAction: () {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, alertText: 'All message sent with the key \'${keytoken.name}\'', filter: MessageFilter(usedKeys: [keytoken.keytokenID])));
|
||||
},
|
||||
),
|
||||
..._buildPermissionCard(context, true, keytoken.toPreview()),
|
||||
UI.button(text: "Delete Key", onPressed: _deleteKey, color: Colors.red[900]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForeignKeyTokenView(BuildContext context, KeyTokenPreview keytoken) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'KeyTokenID',
|
||||
values: [keytoken.keytokenID],
|
||||
),
|
||||
_buildNameCard(context, false),
|
||||
_buildOwnerCard(context, false),
|
||||
..._buildPermissionCard(context, false, keytoken),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOwnerCard(BuildContext context, bool isOwned) {
|
||||
return FutureBuilder(
|
||||
future: _futureOwner.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (AppSettings().showExtendedAttributes)
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'Owner',
|
||||
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
|
||||
);
|
||||
else
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'Owner',
|
||||
values: [(snapshot.data?.username ?? keytokenPreview!.ownerUserID) + (isOwned ? ' (you)' : '')],
|
||||
);
|
||||
} else {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'Owner',
|
||||
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : '')],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameCard(BuildContext context, bool isOwned) {
|
||||
if (_editName == EditState.editing) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: UI.box(
|
||||
context: context,
|
||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _ctrlName,
|
||||
decoration: new InputDecoration.collapsed(hintText: 'Name'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
SizedBox(width: 4),
|
||||
IconButton(icon: FaIcon(FontAwesomeIcons.solidFloppyDisk), onPressed: _saveName),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (_editName == EditState.none) {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidInputText,
|
||||
title: 'Name',
|
||||
values: [_nameOverride ?? keytokenPreview!.name],
|
||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditName)] : [],
|
||||
);
|
||||
} else if (_editName == EditState.saving) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
||||
child: UI.box(
|
||||
context: context,
|
||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
|
||||
SizedBox(width: 16),
|
||||
Expanded(child: SizedBox()),
|
||||
SizedBox(width: 12),
|
||||
SizedBox(width: 4),
|
||||
Padding(padding: const EdgeInsets.all(8.0), child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator())),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
throw 'Invalid EditDisplayNameState: $_editName';
|
||||
}
|
||||
}
|
||||
|
||||
void _showEditName() {
|
||||
setState(() {
|
||||
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
|
||||
_editName = EditState.editing;
|
||||
});
|
||||
}
|
||||
|
||||
void _saveName() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
final newName = _ctrlName.text;
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
_editName = EditState.saving;
|
||||
});
|
||||
|
||||
final newKeyToken = await APIClient.updateKeyToken(userAcc, widget.keytokenID, name: newName);
|
||||
|
||||
setState(() {
|
||||
_editName = EditState.none;
|
||||
_nameOverride = newKeyToken.name;
|
||||
});
|
||||
|
||||
widget.needsReload?.call();
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to save DisplayName');
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserPreview> _getOwner(AppAuth auth) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
_incLoadingIndeterminateCounter(1);
|
||||
|
||||
final owner = APIClient.getUserPreview(auth, keytokenPreview!.ownerUserID);
|
||||
|
||||
//await Future.delayed(const Duration(seconds: 10), () {});
|
||||
|
||||
return owner;
|
||||
} finally {
|
||||
_incLoadingIndeterminateCounter(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void _incLoadingIndeterminateCounter(int delta) {
|
||||
setState(() {
|
||||
_loadingIndeterminateCounter += delta;
|
||||
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> _buildPermissionCard(BuildContext context, bool isOwned, KeyTokenPreview keyToken) {
|
||||
Widget w1;
|
||||
Widget w2;
|
||||
|
||||
if (isOwned) {
|
||||
w1 = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||
title: 'Permissions',
|
||||
values: _formatPermissions(keyToken.permissions),
|
||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
|
||||
);
|
||||
} else {
|
||||
w1 = UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||
title: 'Permissions',
|
||||
values: _formatPermissions(keyToken.permissions),
|
||||
);
|
||||
}
|
||||
|
||||
w2 = FutureBuilder(
|
||||
future: _futureAllChannels.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
var cmap = snapshot.data!;
|
||||
if (isOwned) {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSnake,
|
||||
title: 'Channels',
|
||||
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
||||
);
|
||||
} else {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSnake,
|
||||
title: 'Channels',
|
||||
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isOwned) {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSnake,
|
||||
title: 'Channels',
|
||||
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
||||
);
|
||||
} else {
|
||||
return UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSnake,
|
||||
title: 'Channels',
|
||||
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return [w1, w2];
|
||||
}
|
||||
|
||||
List<String> _formatPermissions(String v) {
|
||||
var splt = v.split(';');
|
||||
|
||||
if (splt.length == 0) return ["None"];
|
||||
|
||||
List<String> result = [];
|
||||
|
||||
if (splt.contains("A")) result.add("Admin");
|
||||
if (splt.contains("UR")) result.add("Read Account");
|
||||
if (splt.contains("CR")) result.add("Read Messages");
|
||||
if (splt.contains("CS")) result.add("Send Messages");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _editPermissions() async {
|
||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot edit the currently used token");
|
||||
return;
|
||||
}
|
||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot edit the currently used token");
|
||||
return;
|
||||
}
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => EditKeyTokenPermissionsDialog(
|
||||
keytoken: keytokenPreview!,
|
||||
onUpdatePermissions: _updatePermissions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editChannels() async {
|
||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot edit the currently used token");
|
||||
return;
|
||||
}
|
||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot edit the currently used token");
|
||||
return;
|
||||
}
|
||||
|
||||
var ownChannels = (await _futureOwnedChannels.future);
|
||||
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => EditKeyTokenChannelsDialog(
|
||||
ownedChannels: ownChannels,
|
||||
keytoken: keytokenPreview!,
|
||||
onUpdateChannels: _updateChannelsSelected,
|
||||
onUpdateSetAllChannels: _updateChannelsAll,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteKey() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot delete the currently used token");
|
||||
return;
|
||||
}
|
||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||
Toaster.error("Error", "You cannot delete the currently used token");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final r = await UIDialogs.showConfirmDialog(context, 'Really (permanently) delete this Key?', okText: 'Delete', cancelText: 'Cancel');
|
||||
if (!r) return;
|
||||
|
||||
await APIClient.deleteKeyToken(acc, keytokenPreview!.keytokenID);
|
||||
widget.needsReload?.call();
|
||||
|
||||
Toaster.info('Logout', 'Successfully deleted the key');
|
||||
|
||||
Navi.pop(context);
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to delete key');
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to delete key');
|
||||
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateChannelsSelected(Set<String> selectedEntries) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
try {
|
||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: selectedEntries.toList(), allChannels: false);
|
||||
|
||||
setState(() {
|
||||
keytoken = r;
|
||||
keytokenPreview = r.toPreview();
|
||||
});
|
||||
|
||||
Toaster.info("Success", "Key updated");
|
||||
|
||||
widget.needsReload?.call();
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to update key');
|
||||
}
|
||||
}
|
||||
|
||||
void _updateChannelsAll() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
try {
|
||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: [], allChannels: true);
|
||||
|
||||
setState(() {
|
||||
keytoken = r;
|
||||
keytokenPreview = r.toPreview();
|
||||
});
|
||||
|
||||
Toaster.info("Success", "Key updated");
|
||||
|
||||
widget.needsReload?.call();
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to update key');
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePermissions(String perm) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
try {
|
||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, permissions: perm);
|
||||
|
||||
setState(() {
|
||||
keytoken = r;
|
||||
keytokenPreview = r.toPreview();
|
||||
});
|
||||
|
||||
Toaster.info("Success", "Key updated");
|
||||
|
||||
widget.needsReload?.call();
|
||||
} on APIException catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
if (!exc.toastShown) Toaster.error("Error", 'Failed to update key');
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to update key');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
enum MessageFilterChipletType {
|
||||
search,
|
||||
plainSearch,
|
||||
channel,
|
||||
sender,
|
||||
timeRange,
|
||||
@@ -21,6 +22,8 @@ class MessageFilterChiplet {
|
||||
switch (type) {
|
||||
case MessageFilterChipletType.search:
|
||||
return FontAwesomeIcons.magnifyingGlass;
|
||||
case MessageFilterChipletType.plainSearch:
|
||||
return FontAwesomeIcons.magnifyingGlassPlus;
|
||||
case MessageFilterChipletType.channel:
|
||||
return FontAwesomeIcons.snake;
|
||||
case MessageFilterChipletType.sender:
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
@@ -30,6 +30,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
||||
|
||||
Map<String, Channel>? _channels = null;
|
||||
bool _channelsFetched = false;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@@ -72,6 +73,9 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
|
||||
_channels = SCNDataCache().getChannelMap();
|
||||
|
||||
//TODO this is not 100% correct - the message-cache contains (which is right!) all messages, even from unsubscribed channels
|
||||
//TODO what we should do is save another list in SCNDataCache, with the result of the last getMessageList call (page-1) and use that
|
||||
//TODO this way we only get 1 page of data from cache, but its a weird behaviour anway that we loose data once _backgroundRefresh is finished
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
|
||||
|
||||
_backgroundRefresh(true);
|
||||
@@ -132,9 +136,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
}
|
||||
|
||||
try {
|
||||
if (_channels == null) {
|
||||
if (_channels == null || !_channelsFetched) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
setState(() {
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
_channelsFetched = true;
|
||||
});
|
||||
|
||||
SCNDataCache().setChannelCache(channels); // no await
|
||||
}
|
||||
@@ -311,6 +318,11 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList();
|
||||
}
|
||||
|
||||
var chipletsPlainSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.plainSearch).toList();
|
||||
if (chipletsPlainSearch.isNotEmpty) {
|
||||
filter.plainSearchFilter = chipletsPlainSearch.map((p) => p.value as String).toList();
|
||||
}
|
||||
|
||||
var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList();
|
||||
if (chipletsKeyTokens.isNotEmpty) {
|
||||
filter.usedKeys = chipletsKeyTokens.map((p) => p.value as String).toList();
|
||||
@@ -326,6 +338,18 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
filter.senderNames = chipletSender.map((p) => p.value as String).toList();
|
||||
}
|
||||
|
||||
var chipletsTimeRange = _filterChiplets.where((p) => p.type == MessageFilterChipletType.timeRange).toList();
|
||||
if (chipletsTimeRange.isNotEmpty) {
|
||||
var t0 = (chipletsTimeRange[0].value as DateTimeRange).start.toLocal();
|
||||
var t1 = (chipletsTimeRange[0].value as DateTimeRange).end.toLocal();
|
||||
|
||||
t0 = DateTime(t0.year, t0.month, t0.day, 0, 0, 0, 0);
|
||||
t1 = DateTime(t1.year, t1.month, t1.day, 23, 59, 59, 999);
|
||||
|
||||
filter.timeAfter = t0;
|
||||
filter.timeBefore = t1;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
class MessageListItem extends StatelessWidget {
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
static final _lineCount = 3; //TODO setting
|
||||
|
||||
const MessageListItem({
|
||||
required this.message,
|
||||
required this.allChannels,
|
||||
@@ -32,6 +30,9 @@ class MessageListItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Card buildWithoutChannel(BuildContext context) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
||||
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
@@ -57,7 +58,7 @@ class MessageListItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
@@ -70,10 +71,10 @@ class MessageListItem extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
processContent(message.content),
|
||||
processContent(message.content, previewLineCount),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: _lineCount,
|
||||
maxLines: previewLineCount,
|
||||
),
|
||||
),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
@@ -90,6 +91,9 @@ class MessageListItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Card buildWithChannel(BuildContext context) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
||||
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
@@ -113,7 +117,7 @@ class MessageListItem extends StatelessWidget {
|
||||
),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(
|
||||
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
@@ -132,10 +136,10 @@ class MessageListItem extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
processContent(message.content),
|
||||
processContent(message.content, previewLineCount),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: _lineCount,
|
||||
maxLines: previewLineCount,
|
||||
),
|
||||
),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
@@ -151,7 +155,7 @@ class MessageListItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String processContent(String? v) {
|
||||
String processContent(String? v, int lineCount) {
|
||||
if (v == null) {
|
||||
return '';
|
||||
}
|
||||
@@ -161,7 +165,7 @@ class MessageListItem extends StatelessWidget {
|
||||
return '';
|
||||
}
|
||||
|
||||
return lines.sublist(0, min(_lineCount, lines.length)).join("\n").trim();
|
||||
return lines.sublist(0, min(lineCount, lines.length)).join("\n").trim();
|
||||
}
|
||||
|
||||
String processTitle(String? v) {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
@@ -34,7 +37,8 @@ class MessageViewPage extends StatefulWidget {
|
||||
class _MessageViewPageState extends State<MessageViewPage> {
|
||||
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
||||
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
|
||||
final ScrollController _controller = ScrollController();
|
||||
|
||||
bool _monospaceMode = false;
|
||||
|
||||
@@ -61,7 +65,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
final msg = await APIClient.getMessage(acc, widget.messageID);
|
||||
|
||||
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
||||
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
|
||||
final fut_key = APIClient.getKeyTokenPreviewByID(acc, msg.usedKeyID);
|
||||
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
||||
|
||||
final chn = await fut_chn;
|
||||
@@ -82,6 +86,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -99,7 +104,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
final (msg, chn, tok, usr) = snapshot.data!;
|
||||
return _buildMessageView(context, msg, chn, tok, usr);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
} else if (message != null && !this.message!.trimmed) {
|
||||
return _buildMessageView(context, this.message!, null, null, null);
|
||||
} else {
|
||||
@@ -136,8 +141,9 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
var cfg = AppSettings();
|
||||
|
||||
final child = Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -146,32 +152,56 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
SizedBox(height: 8),
|
||||
if (message.content != null) ..._buildMessageContent(context, message),
|
||||
SizedBox(height: 8),
|
||||
if (cfg.showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'MessageID',
|
||||
values: [message.messageID, if (message.userMessageID != null) message.userMessageID!],
|
||||
),
|
||||
if (message.senderName != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSignature,
|
||||
title: 'Sender',
|
||||
values: [message.senderName!],
|
||||
mainAction: () => {/*TODO*/},
|
||||
mainAction: () => {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, alertText: 'All message sent from \'${message.senderName!}\'', filter: MessageFilter(senderNames: [message.senderName!])))
|
||||
},
|
||||
),
|
||||
if (cfg.showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidGearCode,
|
||||
title: 'KeyToken',
|
||||
values: [message.usedKeyID, token?.name ?? '...'],
|
||||
mainAction: () => {/*TODO*/},
|
||||
mainAction: () {
|
||||
if (message.senderUserID == userAccUserID) {
|
||||
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
|
||||
} else {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, alertText: 'All message sent with the specified key', filter: MessageFilter(usedKeys: [message.usedKeyID])));
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!cfg.showExtendedAttributes && token != null)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidIdCardClip,
|
||||
title: 'MessageID',
|
||||
values: [message.messageID, message.userMessageID ?? ''],
|
||||
icon: FontAwesomeIcons.solidGearCode,
|
||||
title: 'KeyToken',
|
||||
values: [token.name],
|
||||
mainAction: () {
|
||||
if (message.senderUserID == userAccUserID) {
|
||||
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
|
||||
} else {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: token.name, alertText: 'All message sent with key \'${token.name}\'', filter: MessageFilter(usedKeys: [message.usedKeyID])));
|
||||
}
|
||||
},
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidSnake,
|
||||
title: 'Channel',
|
||||
values: [message.channelID, channel?.displayName ?? message.channelInternalName],
|
||||
values: [if (cfg.showExtendedAttributes) message.channelID, channel?.displayName ?? message.channelInternalName],
|
||||
mainAction: (channel != null)
|
||||
? () {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: channel.channelID, preloadedData: null, needsReload: null));
|
||||
@@ -184,25 +214,64 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
title: 'Timestamp',
|
||||
values: [message.timestamp],
|
||||
),
|
||||
if (cfg.showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'User',
|
||||
values: [user?.userID ?? '...', user?.username ?? ''],
|
||||
mainAction: () => {/*TODO*/},
|
||||
values: [user?.userID ?? message.senderUserID, if (user?.username != null) user?.username ?? ''],
|
||||
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, alertText: 'All message sent by the specified account', filter: MessageFilter(senderUserID: [message.senderUserID]))),
|
||||
),
|
||||
if (!cfg.showExtendedAttributes)
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidUser,
|
||||
title: 'User',
|
||||
values: [user?.username ?? user?.userID ?? message.senderUserID],
|
||||
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, alertText: 'All message sent by the specified account', filter: MessageFilter(senderUserID: [message.senderUserID]))),
|
||||
),
|
||||
UI.metaCard(
|
||||
context: context,
|
||||
icon: FontAwesomeIcons.solidBolt,
|
||||
title: 'Priority',
|
||||
values: [_prettyPrintPriority(message.priority)],
|
||||
mainAction: () => {/*TODO*/},
|
||||
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: "Priority ${message.priority}", alertText: 'All message sent with priority ' + _prettyPrintPriority(message.priority), filter: MessageFilter(priority: [message.priority]))),
|
||||
),
|
||||
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
||||
if (message.senderUserID == userAccUserID)
|
||||
UI.button(
|
||||
text: "Delete Message",
|
||||
onPressed: () {
|
||||
Toaster.info("Not Implemented", "... will be implemented in a later version"); // TODO
|
||||
},
|
||||
color: Colors.red[900]),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
var showScrollbar = false;
|
||||
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
||||
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
||||
|
||||
if (showScrollbar) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 6, 0),
|
||||
child: Scrollbar(
|
||||
thickness: 12.0,
|
||||
radius: Radius.circular(6),
|
||||
thumbVisibility: false,
|
||||
interactive: true,
|
||||
controller: _controller,
|
||||
child: SingleChildScrollView(
|
||||
controller: _controller,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
|
||||
@@ -210,6 +279,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
}
|
||||
|
||||
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
||||
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
@@ -220,7 +291,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
fontSize: 16,
|
||||
),
|
||||
Expanded(child: SizedBox()),
|
||||
Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
||||
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
|
||||
class SendRootPage extends StatefulWidget {
|
||||
const SendRootPage({super.key, required bool isVisiblePage});
|
||||
const SendRootPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
@override
|
||||
State<SendRootPage> createState() => _SendRootPageState();
|
||||
@@ -15,18 +25,28 @@ class SendRootPage extends StatefulWidget {
|
||||
class _SendRootPageState extends State<SendRootPage> {
|
||||
late TextEditingController _msgTitle;
|
||||
late TextEditingController _msgContent;
|
||||
late TextEditingController _channelName;
|
||||
late TextEditingController _senderName;
|
||||
|
||||
int _priority = 0;
|
||||
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_msgTitle = TextEditingController();
|
||||
_msgContent = TextEditingController();
|
||||
_channelName = TextEditingController();
|
||||
_senderName = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_msgTitle.dispose();
|
||||
_msgContent.dispose();
|
||||
_channelName.dispose();
|
||||
_senderName.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -37,7 +57,15 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
child: _expanded ? _buildExpanded(context, acc) : _buildSimple(context, acc),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSimple(BuildContext context, AppAuth acc) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildQRCode(context, acc),
|
||||
@@ -67,22 +95,140 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||
onPressed: _send,
|
||||
child: const Text('Send'),
|
||||
Row(
|
||||
children: [
|
||||
UI.buttonIconOnly(
|
||||
icon: FontAwesomeIcons.webhook,
|
||||
onPressed: _copyCurl,
|
||||
square: true,
|
||||
color: Theme.of(context).colorScheme.secondary.withAlpha(128),
|
||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Send',
|
||||
onPressed: () {
|
||||
_sendSimple(acc);
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
UI.buttonIconOnly(
|
||||
icon: FontAwesomeIcons.layerPlus,
|
||||
onPressed: _openExpanded,
|
||||
square: true,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _send() {
|
||||
//...
|
||||
Widget _buildExpanded(BuildContext context, AppAuth acc) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _channelName,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Channel',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _msgTitle,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Title',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _senderName,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Sender',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SegmentedButton<int>(
|
||||
showSelectedIcon: false,
|
||||
segments: const <ButtonSegment<int>>[
|
||||
ButtonSegment<int>(value: 0, label: Text('Low Priority')),
|
||||
ButtonSegment<int>(value: 1, label: Text('Normal')),
|
||||
ButtonSegment<int>(value: 2, label: Text('High Priority')),
|
||||
],
|
||||
selected: {_priority},
|
||||
onSelectionChanged: (Set<int> newSelection) {
|
||||
setState(() {
|
||||
_priority = newSelection.isEmpty ? 1 : newSelection.first;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 1.0,
|
||||
child: TextField(
|
||||
controller: _msgContent,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'Text',
|
||||
),
|
||||
minLines: 6,
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
UI.buttonIconOnly(
|
||||
icon: FontAwesomeIcons.webhook,
|
||||
onPressed: _copyCurl,
|
||||
square: true,
|
||||
color: Theme.of(context).colorScheme.secondary.withAlpha(128),
|
||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: UI.button(
|
||||
text: 'Send',
|
||||
onPressed: () {
|
||||
_sendExpanded(acc);
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
UI.buttonIconOnly(
|
||||
icon: FontAwesomeIcons.squareDashed,
|
||||
onPressed: _closeExpanded,
|
||||
square: true,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQRCode(BuildContext context, AppAuth acc) {
|
||||
@@ -93,11 +239,22 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
return FutureBuilder(
|
||||
future: acc.loadUser(force: false),
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
if (snapshot.connectionState == ConnectionState.active || snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
width: 300.0,
|
||||
height: 300.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||
}
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return Text('...'); //?
|
||||
}
|
||||
|
||||
var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_openWeb(url);
|
||||
@@ -116,16 +273,54 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox(
|
||||
width: 300.0,
|
||||
height: 300.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendSimple(AppAuth acc) async {
|
||||
if (!acc.isAuth()) {
|
||||
Toaster.error("Error", 'Must be logged in to send messages');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text);
|
||||
Toaster.success("Success", 'Message sent');
|
||||
setState(() {
|
||||
_msgTitle.clear();
|
||||
_msgContent.clear();
|
||||
});
|
||||
} on APIException catch (e, stackTrace) {
|
||||
if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendExpanded(AppAuth acc) async {
|
||||
if (!acc.isAuth()) {
|
||||
Toaster.error("Error", 'Must be logged in to send messages');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text, channel: _channelName.text, senderName: _senderName.text, priority: _priority);
|
||||
Toaster.success("Success", 'Message sent');
|
||||
setState(() {
|
||||
_msgTitle.clear();
|
||||
_msgContent.clear();
|
||||
});
|
||||
} on APIException catch (e, stackTrace) {
|
||||
if (!e.toastShown) Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||
} catch (e, stackTrace) {
|
||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void _openWeb(String url) async {
|
||||
try {
|
||||
final Uri uri = Uri.parse(url);
|
||||
@@ -135,10 +330,61 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
// TODO ("Cannot open URL");
|
||||
Toaster.error("Error", 'Cannot open URL on this system');
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
void _closeExpanded() {
|
||||
setState(() {
|
||||
_expanded = false;
|
||||
_channelName.clear();
|
||||
_priority = 1;
|
||||
_senderName.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _openExpanded() {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
setState(() {
|
||||
_expanded = true;
|
||||
_channelName.text = userAcc.getUserOrNull()?.defaultChannel ?? 'main';
|
||||
_priority = 1;
|
||||
_senderName.text = Globals().deviceName;
|
||||
});
|
||||
}
|
||||
|
||||
void _copyCurl() {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (!userAcc.isAuth()) {
|
||||
Toaster.error("Error", 'Must be logged in to send messages');
|
||||
return;
|
||||
}
|
||||
|
||||
String curl = 'curl';
|
||||
curl += ' --data user_id="${userAcc.userID}"';
|
||||
curl += ' --data key="${userAcc.tokenSend}"';
|
||||
curl += ' --data title="${shellEscape(_msgTitle.text)}"';
|
||||
curl += ' --data content="${shellEscape(_msgContent.text)}"';
|
||||
|
||||
if (_expanded) {
|
||||
curl += ' --data channel="${shellEscape(_channelName.text)}"';
|
||||
curl += ' --data sender_name="${shellEscape(_senderName.text)}"';
|
||||
curl += ' --data priority="${_priority}"';
|
||||
}
|
||||
|
||||
curl += ' "https://simplecloudnotifier.de/"';
|
||||
|
||||
Clipboard.setData(new ClipboardData(text: curl));
|
||||
Toaster.info("Clipboard", 'Copied curl-command to Clipboard');
|
||||
print('================= [CLIPBOARD] =================\n${curl}\n================= [/CLIPBOARD] =================');
|
||||
}
|
||||
|
||||
String shellEscape(String str) {
|
||||
return str.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n').replaceAll('\t', '\\t');
|
||||
}
|
||||
}
|
||||
|
||||
107
flutter/lib/pages/sender_list/sender_list.dart
Normal file
107
flutter/lib/pages/sender_list/sender_list.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart';
|
||||
|
||||
class SenderListPage extends StatefulWidget {
|
||||
const SenderListPage({super.key});
|
||||
|
||||
@override
|
||||
State<SenderListPage> createState() => _SenderListPageState();
|
||||
}
|
||||
|
||||
class _SenderListPageState extends State<SenderListPage> {
|
||||
final PagingController<int, SenderNameStatistics> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('SenderListPage::dispose');
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start SenderListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = (await APIClient.getSenderNameList(acc)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * a.lastTimestamp.compareTo(b.lastTimestamp));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list senders: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Sender",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
BadgeDisplay(
|
||||
text: "All sender used to send messages to this account",
|
||||
icon: null,
|
||||
mode: BadgeMode.info,
|
||||
textAlign: TextAlign.left,
|
||||
closable: true,
|
||||
extraPadding: EdgeInsets.fromLTRB(0, 0, 0, 16),
|
||||
hidden: !AppSettings().showInfoAlerts,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildList(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<int, SenderNameStatistics>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>(
|
||||
itemBuilder: (context, item, index) => SenderListItem(item: item),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
flutter/lib/pages/sender_list/sender_list_item.dart
Normal file
87
flutter/lib/pages/sender_list/sender_list_item.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
enum SenderListItemMode {
|
||||
Messages,
|
||||
Extended,
|
||||
}
|
||||
|
||||
class SenderListItem extends StatelessWidget {
|
||||
const SenderListItem({
|
||||
required this.item,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SenderNameStatistics item;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
||||
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name])));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(FontAwesomeIcons.solidSignature, color: Theme.of(context).colorScheme.outline, size: 32),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
dateFormat.format(DateTime.parse(item.lastTimestamp).toLocal()),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
),
|
||||
),
|
||||
Text(item.count.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, alertText: 'All message sent from \'${item.name!}\'', filter: MessageFilter(senderNames: [item.name])));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsRootPage extends StatefulWidget {
|
||||
const SettingsRootPage({super.key, required bool isVisiblePage});
|
||||
|
||||
@override
|
||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||
}
|
||||
|
||||
class _SettingsRootPageState extends State<SettingsRootPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Text('Settings'),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
flutter/lib/pages/settings/settings_number_modal.dart
Normal file
117
flutter/lib/pages/settings/settings_number_modal.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class SettingsNumberModal extends StatefulWidget {
|
||||
final String title;
|
||||
final int currentValue;
|
||||
final int minValue;
|
||||
final int maxValue;
|
||||
final ValueChanged<int> onValueChanged;
|
||||
|
||||
const SettingsNumberModal({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.currentValue,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.onValueChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SettingsNumberModal> createState() => _SettingsNumberModalState();
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required int currentValue,
|
||||
required int minValue,
|
||||
required int maxValue,
|
||||
required ValueChanged<int> onValueChanged,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => SettingsNumberModal(
|
||||
title: title,
|
||||
currentValue: currentValue,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
onValueChanged: onValueChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsNumberModalState extends State<SettingsNumberModal> {
|
||||
late TextEditingController _controller;
|
||||
late int selectedValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedValue = widget.currentValue;
|
||||
_controller = TextEditingController(text: widget.currentValue.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Enter a number',
|
||||
errorText: _validateInput(),
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedValue = int.tryParse(value) ?? widget.currentValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _validateInput() == null
|
||||
? () {
|
||||
widget.onValueChanged(selectedValue);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? _validateInput() {
|
||||
final number = int.tryParse(_controller.text);
|
||||
if (number == null) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
if (number < widget.minValue) {
|
||||
return 'Value must be at least ${widget.minValue}';
|
||||
}
|
||||
if (number > widget.maxValue) {
|
||||
return 'Value must be at most ${widget.maxValue}';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user