Compare commits
12 Commits
test/max_o
...
flutter_ap
Author | SHA1 | Date | |
---|---|---|---|
9c53cc52e9
|
|||
e6709cd4af
|
|||
cdb92757aa
|
|||
3c5da802a7
|
|||
05e2fcf185
|
|||
8ebd95a4b8
|
|||
80d4e18a23
|
|||
cc672d2f20
|
|||
1cf14e65a9
|
|||
9b2e429d3d
|
|||
2f73a21a41
|
|||
05eb37bc80
|
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
|
targetCompatibility 1.8
|
||||||
sourceCompatibility 1.8
|
sourceCompatibility 1.8
|
||||||
}
|
}
|
||||||
|
namespace 'com.blackforestbytes.simplecloudnotifier'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.blackforestbytes.simplecloudnotifier">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
@@ -7,8 +7,8 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||||
classpath 'com.google.gms:google-services:4.3.4'
|
classpath 'com.google.gms:google-services:4.3.10'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,3 +14,6 @@ org.gradle.jvmargs=-Xmx1536m
|
|||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=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
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
2
flutter/.gitignore
vendored
2
flutter/.gitignore
vendored
@@ -5,6 +5,8 @@
|
|||||||
firepit-log.txt
|
firepit-log.txt
|
||||||
flutter_jank_*
|
flutter_jank_*
|
||||||
|
|
||||||
|
_releases/*
|
||||||
|
|
||||||
|
|
||||||
#######################################################################################################################
|
#######################################################################################################################
|
||||||
|
|
||||||
|
@@ -1,15 +1,40 @@
|
|||||||
|
|
||||||
|
|
||||||
run:
|
# Setup
|
||||||
flutter pub run build_runner build
|
#
|
||||||
flutter run
|
# flutter config --jdk-dir "/usr/lib/jvm/default-runtime/bin"
|
||||||
|
# sudo archlinux-java set java-17-openjdk
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# runs app locally (linux)
|
||||||
|
run-linux:
|
||||||
|
dart run build_runner build
|
||||||
|
_JAVA_OPTIONS="" flutter run -d linux
|
||||||
|
|
||||||
|
# runs app locally (web | not really supported)
|
||||||
|
run-linux:
|
||||||
|
dart run build_runner build
|
||||||
|
_JAVA_OPTIONS="" flutter run -d web
|
||||||
|
|
||||||
|
# runs on android device (must have network adb enabled teh correct IP)
|
||||||
run-android:
|
run-android:
|
||||||
ping -c1 10.10.10.177
|
ping -c1 10.10.10.177
|
||||||
adb connect 10.10.10.177:5555
|
adb connect 10.10.10.177:5555
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
flutter run -d 10.10.10.177:5555
|
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
|
||||||
|
|
||||||
|
install-release:
|
||||||
|
# Install on Pixel 7a
|
||||||
|
flutter build apk --release
|
||||||
|
flutter run --release -d 35221JEHN07157
|
||||||
|
|
||||||
|
build-release:
|
||||||
|
flutter build apk --release
|
||||||
|
flutter build appbundle --release
|
||||||
|
flutter build linux --release
|
||||||
|
|
||||||
test:
|
test:
|
||||||
dart analyze
|
dart analyze
|
||||||
|
|
||||||
@@ -18,10 +43,24 @@ fix:
|
|||||||
|
|
||||||
gen:
|
gen:
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
|
|
||||||
|
# run `make run` in another terminal (or another variant of flutter run)
|
||||||
autoreload:
|
autoreload:
|
||||||
@# run `make run` in another terminal (or another variant of flutter run)
|
@
|
||||||
@_utils/autoreload.sh
|
@_utils/autoreload.sh
|
||||||
|
|
||||||
icons:
|
icons:
|
||||||
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
|
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cd android && ./gradlew clean
|
||||||
|
flutter 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
|
@@ -25,6 +25,15 @@
|
|||||||
|
|
||||||
- [ ] 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?
|
- [ ] 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?
|
||||||
|
|
||||||
|
- [ ] fix time format (in message-list, in card, top right) - midnight is shown as "24:05" instead of "00:05" - thats weird
|
||||||
|
|
||||||
|
- [ ] Add scrollbar
|
||||||
|
-> https://api.flutter.dev/flutter/material/Scrollbar-class.html
|
||||||
|
|
||||||
|
- [ ] 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
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
# TODO iOS specific
|
# TODO iOS specific
|
||||||
|
4
flutter/android/.gitignore
vendored
4
flutter/android/.gitignore
vendored
@@ -11,3 +11,7 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
|
||||||
|
build/
|
||||||
|
|
||||||
|
app/.cxx/
|
@@ -34,16 +34,16 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace "com.blackforestbytes.simplecloudnotifier"
|
namespace "com.blackforestbytes.simplecloudnotifier"
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion "27.0.12077973" // should be `flutter.ndkVersion` - but some plugins need 27, even though flutter still has the default value of 26 (flutter 3.29 | 2025-04-12)
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '17'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
|
||||||
|
@@ -23,7 +23,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
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
|
// START: FlutterFire Configuration
|
||||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
|
@@ -45,5 +45,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<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>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_exception.dart';
|
|||||||
import 'package:simplecloudnotifier/models/api_error.dart';
|
import 'package:simplecloudnotifier/models/api_error.dart';
|
||||||
import 'package:simplecloudnotifier/models/client.dart';
|
import 'package:simplecloudnotifier/models/client.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
@@ -101,19 +102,21 @@ class APIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (responseStatusCode != 200) {
|
if (responseStatusCode != 200) {
|
||||||
try {
|
APIError apierr;
|
||||||
final apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
|
|
||||||
|
|
||||||
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
|
try {
|
||||||
Toaster.error("Error", 'Request "${name}" failed');
|
apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
|
||||||
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
|
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: 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.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
|
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
|
||||||
Toaster.error("Error", 'Request "${name}" failed');
|
Toaster.error("Error", apierr.message);
|
||||||
throw Exception('API request failed with status code ${responseStatusCode}');
|
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -159,6 +162,20 @@ 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<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
|
static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'addClient',
|
name: 'addClient',
|
||||||
@@ -281,6 +298,20 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<List<Subscription>> getSubscriptionList(TokenSource auth) async {
|
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getSubscriptionList',
|
name: 'getSubscriptionList',
|
||||||
@@ -369,7 +400,62 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<String>> getSenderNameList(AppAuth userAcc) {
|
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
|
||||||
return Future.value(['TODO']); //TODO
|
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) async {
|
||||||
|
return await _request(
|
||||||
|
name: 'subscribeToChannelbyID',
|
||||||
|
method: 'POST',
|
||||||
|
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||||
|
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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
class APIException implements Exception {
|
class APIException implements Exception {
|
||||||
final int httpStatus;
|
final int httpStatus;
|
||||||
final int error;
|
final int error;
|
||||||
final String errHighlight;
|
final int errHighlight;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
APIException(this.httpStatus, this.error, this.errHighlight, this.message);
|
APIException(this.httpStatus, this.error, this.errHighlight, this.message);
|
||||||
|
@@ -25,7 +25,7 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
|
|||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
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;
|
return senders;
|
||||||
}());
|
}());
|
||||||
|
@@ -1,10 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class FilterModalTime extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
|
@@ -59,6 +59,7 @@ void main() async {
|
|||||||
Hive.deleteBoxFromDisk('scn-logs');
|
Hive.deleteBoxFromDisk('scn-logs');
|
||||||
await Hive.openBox<SCNLog>('scn-logs');
|
await Hive.openBox<SCNLog>('scn-logs');
|
||||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
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});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-requests>...');
|
print('[INIT] Load Hive<scn-requests>...');
|
||||||
@@ -69,6 +70,7 @@ void main() async {
|
|||||||
Hive.deleteBoxFromDisk('scn-requests');
|
Hive.deleteBoxFromDisk('scn-requests');
|
||||||
await Hive.openBox<SCNRequest>('scn-requests');
|
await Hive.openBox<SCNRequest>('scn-requests');
|
||||||
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
|
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});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-message-cache>...');
|
print('[INIT] Load Hive<scn-message-cache>...');
|
||||||
@@ -79,6 +81,7 @@ void main() async {
|
|||||||
Hive.deleteBoxFromDisk('scn-message-cache');
|
Hive.deleteBoxFromDisk('scn-message-cache');
|
||||||
await Hive.openBox<SCNMessage>('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.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});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-channel-cache>...');
|
print('[INIT] Load Hive<scn-channel-cache>...');
|
||||||
@@ -89,6 +92,7 @@ void main() async {
|
|||||||
Hive.deleteBoxFromDisk('scn-channel-cache');
|
Hive.deleteBoxFromDisk('scn-channel-cache');
|
||||||
await Hive.openBox<Channel>('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.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});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-fb-messages>...');
|
print('[INIT] Load Hive<scn-fb-messages>...');
|
||||||
@@ -99,6 +103,7 @@ void main() async {
|
|||||||
Hive.deleteBoxFromDisk('scn-fb-messages');
|
Hive.deleteBoxFromDisk('scn-fb-messages');
|
||||||
await Hive.openBox<FBMessage>('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.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});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load AppAuth...');
|
print('[INIT] Load AppAuth...');
|
||||||
@@ -112,11 +117,13 @@ void main() async {
|
|||||||
await appAuth.loadUser();
|
await appAuth.loadUser();
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: 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 {
|
try {
|
||||||
await appAuth.loadClient();
|
await appAuth.loadClient();
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: 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});
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
@@ -219,7 +226,7 @@ class SCNApp extends StatelessWidget {
|
|||||||
return ToastificationWrapper(
|
return ToastificationWrapper(
|
||||||
config: ToastificationConfig(
|
config: ToastificationConfig(
|
||||||
itemWidth: 440,
|
itemWidth: 440,
|
||||||
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
|
marginBuilder: (context, alignment) => EdgeInsets.symmetric(vertical: 64),
|
||||||
animationDuration: Duration(milliseconds: 200),
|
animationDuration: Duration(milliseconds: 200),
|
||||||
),
|
),
|
||||||
child: Consumer<AppTheme>(
|
child: Consumer<AppTheme>(
|
||||||
@@ -321,6 +328,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
await Hive.openBox<SCNMessage>('scn-message-cache');
|
await Hive.openBox<SCNMessage>('scn-message-cache');
|
||||||
await Hive.openBox<SCNRequest>('scn-requests');
|
await Hive.openBox<SCNRequest>('scn-requests');
|
||||||
} catch (exc, trace) {
|
} 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);
|
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);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
|
||||||
return;
|
return;
|
||||||
@@ -341,6 +349,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
|
|
||||||
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
|
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
|
||||||
} catch (exc, trace) {
|
} 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);
|
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);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
|
||||||
return;
|
return;
|
||||||
@@ -349,6 +358,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
try {
|
try {
|
||||||
FBMessageLog.insert(message);
|
FBMessageLog.insert(message);
|
||||||
} catch (exc, trace) {
|
} 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);
|
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);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
|
||||||
return;
|
return;
|
||||||
@@ -359,6 +369,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
SCNDataCache().addToMessageCache([msg]);
|
SCNDataCache().addToMessageCache([msg]);
|
||||||
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
|
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
|
||||||
} catch (exc, trace) {
|
} 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);
|
ApplicationLog.error('Failed to query+persist message: ' + exc.toString(), trace: trace);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -382,7 +393,7 @@ void _handleNotificationClickAction(String? payload, Duration delay) {
|
|||||||
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
|
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
|
||||||
final messageID = parts[1];
|
final messageID = parts[1];
|
||||||
() async {
|
() async {
|
||||||
await Future.delayed(delay);
|
await Future.delayed(delay, () {});
|
||||||
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
|
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
|
||||||
@@ -392,7 +403,7 @@ void _handleNotificationClickAction(String? payload, Duration delay) {
|
|||||||
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
|
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
|
||||||
final channelID = parts[1];
|
final channelID = parts[1];
|
||||||
() async {
|
() async {
|
||||||
await Future.delayed(delay);
|
await Future.delayed(delay, () {});
|
||||||
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');
|
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
class APIError {
|
class APIError {
|
||||||
final bool success;
|
final bool success;
|
||||||
final int error;
|
final int error;
|
||||||
final String errhighlight;
|
final int errhighlight;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
static final MISSING_UID = 1101;
|
static final MISSING_UID = 1101;
|
||||||
@@ -67,7 +67,7 @@ class APIError {
|
|||||||
return APIError(
|
return APIError(
|
||||||
success: json['success'] as bool,
|
success: json['success'] as bool,
|
||||||
error: (json['error'] as num).toInt(),
|
error: (json['error'] as num).toInt(),
|
||||||
errhighlight: json['errhighlight'] as String,
|
errhighlight: (json['errhighlight'] as num).toInt(),
|
||||||
message: json['message'] as String,
|
message: json['message'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
75
flutter/lib/models/scan_result.dart
Normal file
75
flutter/lib/models/scan_result.dart
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
|
||||||
|
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
|
||||||
|
|
||||||
|
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']);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != 4) return null;
|
||||||
|
|
||||||
|
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
ScanResultMessageSend({required this.userID, required this.userKey});
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
@@ -7,11 +7,13 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/account/login.dart';
|
import 'package:simplecloudnotifier/pages/account/login.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
import 'package:simplecloudnotifier/state/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
@@ -32,6 +34,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
late ImmediateFuture<int>? futureKeyCount;
|
late ImmediateFuture<int>? futureKeyCount;
|
||||||
late ImmediateFuture<int>? futureChannelAllCount;
|
late ImmediateFuture<int>? futureChannelAllCount;
|
||||||
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||||
|
late ImmediateFuture<int>? futureSenderNamesCount;
|
||||||
late ImmediateFuture<User>? futureUser;
|
late ImmediateFuture<User>? futureUser;
|
||||||
|
|
||||||
late AppAuth userAcc;
|
late AppAuth userAcc;
|
||||||
@@ -87,6 +90,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
futureKeyCount = null;
|
futureKeyCount = null;
|
||||||
futureChannelAllCount = null;
|
futureChannelAllCount = null;
|
||||||
futureChannelSubscribedCount = null;
|
futureChannelSubscribedCount = null;
|
||||||
|
futureSenderNamesCount = null;
|
||||||
|
|
||||||
if (userAcc.isAuth()) {
|
if (userAcc.isAuth()) {
|
||||||
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||||
@@ -119,6 +123,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
return keys.length;
|
return keys.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
|
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));
|
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +147,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
|
final senderNames = await APIClient.getSenderNameList(userAcc);
|
||||||
final user = await userAcc.loadUser(force: true);
|
final user = await userAcc.loadUser(force: true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -145,6 +156,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||||
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||||
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||||
|
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
||||||
futureUser = ImmediateFuture.ofValue(user);
|
futureUser = ImmediateFuture.ofValue(user);
|
||||||
});
|
});
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
@@ -348,13 +360,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
UI.buttonIconOnly(
|
UI.buttonIconOnly(
|
||||||
onPressed: () {/*TODO*/},
|
onPressed: _changeUsername,
|
||||||
icon: FontAwesomeIcons.pen,
|
icon: FontAwesomeIcons.pen,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (!user.isPro)
|
if (!user.isPro)
|
||||||
UI.buttonIconOnly(
|
UI.buttonIconOnly(
|
||||||
onPressed: () {/*TODO*/},
|
onPressed: () {
|
||||||
|
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
|
||||||
|
},
|
||||||
icon: FontAwesomeIcons.cartCircleArrowUp,
|
icon: FontAwesomeIcons.cartCircleArrowUp,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -368,7 +382,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
|
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {
|
||||||
|
Navi.push(context, () => ChannelListExtendedPage());
|
||||||
|
}),
|
||||||
|
_buildNumberCard(context, 'Sender', futureSenderNamesCount, () {/*TODO*/}),
|
||||||
UI.buttonCard(
|
UI.buttonCard(
|
||||||
context: context,
|
context: context,
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||||
@@ -488,4 +505,31 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
void _deleteAccount() async {
|
void _deleteAccount() async {
|
||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Toaster.error("Error", 'Failed to update username');
|
||||||
|
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -140,13 +140,15 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final toks = await APIClient.getKeyTokenByToken(uid, stokv);
|
if (stokv != "") {
|
||||||
|
final toks = await APIClient.getKeyTokenByToken(uid, stokv);
|
||||||
|
|
||||||
if (!toks.allChannels || toks.permissions != 'CS') {
|
if (!toks.allChannels || toks.permissions != 'CS') {
|
||||||
Toaster.error("Error", 'Send token does not have required permissions');
|
Toaster.error("Error", 'Send token does not have required permissions');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
|
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
|
||||||
|
|
||||||
final client = await APIClient.addClient(DirectTokenSource(uid, atokv), fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
|
final client = await APIClient.addClient(DirectTokenSource(uid, atokv), fcmToken, Globals().deviceModel, Globals().version, Globals().hostname, Globals().clientType);
|
||||||
|
@@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_list/channel_scanner.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
@@ -154,8 +154,13 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
|||||||
itemBuilder: (context, item, index) => ChannelListItem(
|
itemBuilder: (context, item, index) => ChannelListItem(
|
||||||
channel: item.channel,
|
channel: item.channel,
|
||||||
subscription: item.subscription,
|
subscription: item.subscription,
|
||||||
onPressed: () {
|
mode: ChannelListItemMode.Messages,
|
||||||
Navi.push(context, () => ChannelViewPage(channelID: item.channel.channelID, preloadedData: (item.channel, item.subscription), needsReload: _enqueueReload));
|
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);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -164,7 +169,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
|||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'fab_channel_list_qr',
|
heroTag: 'fab_channel_list_qr',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//TODO scan qr code to subscribe channel
|
Navi.push(context, () => ChannelScannerPage());
|
||||||
},
|
},
|
||||||
child: const Icon(FontAwesomeIcons.qrcode),
|
child: const Icon(FontAwesomeIcons.qrcode),
|
||||||
),
|
),
|
||||||
|
151
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
151
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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/layout/scaffold.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_bar_state.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: 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,6 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -9,24 +7,35 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
|||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.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_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
|
||||||
|
enum ChannelListItemMode {
|
||||||
|
Messages,
|
||||||
|
Extended,
|
||||||
|
}
|
||||||
|
|
||||||
class ChannelListItem extends StatefulWidget {
|
class ChannelListItem extends StatefulWidget {
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
const ChannelListItem({
|
const ChannelListItem({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.onPressed,
|
required this.onChannelListReloadTrigger,
|
||||||
|
required this.onSubscriptionChanged,
|
||||||
required this.subscription,
|
required this.subscription,
|
||||||
|
required this.mode,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
final Subscription? subscription;
|
final Subscription? subscription;
|
||||||
final Null Function() onPressed;
|
final void Function() onChannelListReloadTrigger;
|
||||||
|
final ChannelListItemMode mode;
|
||||||
|
final void Function(String, Subscription?) onSubscriptionChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChannelListItem> createState() => _ChannelListItemState();
|
State<ChannelListItem> createState() => _ChannelListItemState();
|
||||||
@@ -41,11 +50,11 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
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;
|
lastMessage = SCNDataCache().getMessagesSorted().where((p) => p.channelID == widget.channel.channelID).firstOrNull;
|
||||||
|
|
||||||
() async {
|
() 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(() {
|
setState(() {
|
||||||
lastMessage = channelMessages.firstOrNull;
|
lastMessage = channelMessages.firstOrNull;
|
||||||
});
|
});
|
||||||
@@ -55,13 +64,18 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
//TODO subscription status
|
|
||||||
return Card.filled(
|
return Card.filled(
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||||
color: Theme.of(context).cardTheme.color,
|
color: Theme.of(context).cardTheme.color,
|
||||||
child: InkWell(
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -90,13 +104,8 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: (widget.mode == ChannelListItemMode.Messages) ? Text(_preformatTitle(lastMessage), style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))) : _buildSubscriptionStateText(context)),
|
||||||
child: Text(
|
(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)),
|
||||||
_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)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -105,11 +114,15 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -126,13 +139,73 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
|
|
||||||
Widget _buildIcon(BuildContext context) {
|
Widget _buildIcon(BuildContext context) {
|
||||||
if (widget.subscription == null) {
|
if (widget.subscription == null) {
|
||||||
return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
Widget result = Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
||||||
|
result = GestureDetector(onTap: () => _subscribe(), child: result);
|
||||||
|
return result;
|
||||||
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
} 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)
|
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
|
||||||
|
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||||
|
return result;
|
||||||
} else if (widget.subscription!.confirmed) {
|
} else if (widget.subscription!.confirmed) {
|
||||||
return Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
|
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
|
||||||
|
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||||
|
return result;
|
||||||
} else {
|
} else {
|
||||||
return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
||||||
|
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||||
|
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||||
|
} else if (widget.subscription!.confirmed) {
|
||||||
|
return Text("subscripted (foreign channe)", 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');
|
||||||
|
}
|
||||||
|
} 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() && widget.channel.ownerUserID == acc.getUserID() && widget.subscription != null) {
|
||||||
|
try {
|
||||||
|
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
|
||||||
|
widget.onChannelListReloadTrigger.call();
|
||||||
|
|
||||||
|
widget.onSubscriptionChanged.call(widget.channel.channelID, null);
|
||||||
|
|
||||||
|
Toaster.success("Success", 'Unsubscribed from channel');
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||||
|
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
117
flutter/lib/pages/channel_list/channel_scanner.dart
Normal file
117
flutter/lib/pages/channel_list/channel_scanner.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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/layout/scaffold.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/scan_result.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),
|
||||||
|
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),
|
||||||
|
_buildScanResult(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBarcode(BarcodeCapture barcodes) {
|
||||||
|
setState(() {
|
||||||
|
if (barcodes.barcodes.isEmpty) {
|
||||||
|
scanResult = null;
|
||||||
|
} else {
|
||||||
|
print('parsed: ${barcodes.barcodes[0].rawValue}');
|
||||||
|
scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanResult(BuildContext context) {
|
||||||
|
if (scanResult == null) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2), //TODO
|
||||||
|
context: context,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultMessageSend) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||||
|
context: context,
|
||||||
|
child: Text("TODO -- ScanResultMessageSend"), //TODO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultChannel) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||||
|
context: context,
|
||||||
|
child: Text("TODO -- ScanResultChannel"), //TODO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultChannelSubscribe) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||||
|
context: context,
|
||||||
|
child: Text("TODO -- ScanResultChannelSubscribe"), //TODO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||||
|
context: context,
|
||||||
|
child: Text("TODO -- ERROR"), //TODO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -55,7 +55,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
SCNDataCache().addToMessageCache(newItems); // no await
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
||||||
@@ -63,16 +64,15 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_initStateAsync();
|
_initStateAsync(true);
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _initStateAsync(bool usePreload) async {
|
||||||
void _initStateAsync() async {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
if (widget.preloadedData != null) {
|
if (widget.preloadedData != null && usePreload) {
|
||||||
channelPreview = widget.preloadedData!.$1.toPreview();
|
channelPreview = widget.preloadedData!.$1.toPreview();
|
||||||
channel = widget.preloadedData!.$1;
|
channel = widget.preloadedData!.$1;
|
||||||
subscription = widget.preloadedData!.$2;
|
subscription = widget.preloadedData!.$2;
|
||||||
@@ -232,7 +232,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||||
title: 'Subscription (own)',
|
title: 'Subscription (foreign)',
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
values: [_formatSubscriptionStatus(subscription)],
|
||||||
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
||||||
),
|
),
|
||||||
@@ -296,8 +296,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: _futureSubscribeKey.future,
|
future: _futureSubscribeKey.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData && snapshot.data != null) {
|
if (snapshot.hasData) {
|
||||||
var text = 'TODO' + '\n' + channel!.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
|
final text = (snapshot.data == null) ? ScanResult.createChannelQR(channel!) : ScanResult.createChannelSubscribeQR(channel!, snapshot.data!);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
|
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
|
||||||
@@ -306,7 +306,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
child: QrImageView(
|
child: QrImageView(
|
||||||
data: text,
|
data: text,
|
||||||
version: QrVersions.auto,
|
version: QrVersions.auto,
|
||||||
size: 300.0,
|
size: 265.0,
|
||||||
eyeStyle: QrEyeStyle(
|
eyeStyle: QrEyeStyle(
|
||||||
eyeShape: QrEyeShape.square,
|
eyeShape: QrEyeShape.square,
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
@@ -318,12 +318,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 {
|
} else {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
width: 300.0,
|
width: 300.0,
|
||||||
@@ -447,14 +441,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _subscribe() {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unsubscribe() {
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showEditDisplayName() {
|
void _showEditDisplayName() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_ctrlDisplayName.text = _displayNameOverride ?? channelPreview?.displayName ?? '';
|
_ctrlDisplayName.text = _displayNameOverride ?? channelPreview?.displayName ?? '';
|
||||||
@@ -519,16 +505,90 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelForeignSubscription(Subscription sub) {
|
void _subscribe() async {
|
||||||
//TODO
|
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');
|
||||||
|
}
|
||||||
|
} 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) {
|
void _unsubscribe() async {
|
||||||
//TODO
|
final acc = AppAuth();
|
||||||
|
|
||||||
|
if (subscription == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||||
|
widget.needsReload?.call();
|
||||||
|
|
||||||
|
await _initStateAsync(false);
|
||||||
|
|
||||||
|
Toaster.success("Success", 'Unsubscribed from channel');
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||||
|
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _denyForeignSubscription(Subscription sub) {
|
void _cancelForeignSubscription(Subscription sub) async {
|
||||||
//TODO
|
final acc = AppAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await APIClient.unconfirmSubscription(acc, widget.channelID, sub.subscriptionID);
|
||||||
|
widget.needsReload?.call();
|
||||||
|
|
||||||
|
await _initStateAsync(false);
|
||||||
|
|
||||||
|
Toaster.success("Success", 'Subscription succesfully revoked');
|
||||||
|
} 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');
|
||||||
|
} 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');
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Toaster.error("Error", 'Failed to deny subscription');
|
||||||
|
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatSubscriptionStatus(Subscription? subscription) {
|
String _formatSubscriptionStatus(Subscription? subscription) {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/utils/notifier.dart';
|
import 'package:simplecloudnotifier/utils/notifier.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
@@ -55,11 +55,11 @@ class _DebugColorsPageState extends State<DebugColorsPage> {
|
|||||||
buildCol("colorScheme.surface", Theme.of(context).colorScheme.surface),
|
buildCol("colorScheme.surface", Theme.of(context).colorScheme.surface),
|
||||||
buildCol("colorScheme.onSurface", Theme.of(context).colorScheme.onSurface),
|
buildCol("colorScheme.onSurface", Theme.of(context).colorScheme.onSurface),
|
||||||
buildCol("colorScheme.surfaceTint", Theme.of(context).colorScheme.surfaceTint),
|
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.inverseSurface", Theme.of(context).colorScheme.inverseSurface),
|
||||||
buildCol("colorScheme.onInverseSurface", Theme.of(context).colorScheme.onInverseSurface),
|
buildCol("colorScheme.onInverseSurface", Theme.of(context).colorScheme.onInverseSurface),
|
||||||
buildCol("colorScheme.background", Theme.of(context).colorScheme.background),
|
buildCol("colorScheme.background", Theme.of(context).colorScheme.surface),
|
||||||
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onBackground),
|
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onSurface),
|
||||||
buildCol("colorScheme.error", Theme.of(context).colorScheme.error),
|
buildCol("colorScheme.error", Theme.of(context).colorScheme.error),
|
||||||
buildCol("colorScheme.onError", Theme.of(context).colorScheme.onError),
|
buildCol("colorScheme.onError", Theme.of(context).colorScheme.onError),
|
||||||
buildCol("colorScheme.errorContainer", Theme.of(context).colorScheme.errorContainer),
|
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("badgeTheme.backgroundColor", Theme.of(context).badgeTheme.backgroundColor),
|
||||||
buildCol("bannerTheme.backgroundColor", Theme.of(context).bannerTheme.backgroundColor),
|
buildCol("bannerTheme.backgroundColor", Theme.of(context).bannerTheme.backgroundColor),
|
||||||
buildCol("bottomAppBarTheme.color", Theme.of(context).bottomAppBarTheme.color),
|
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.primary", Theme.of(context).buttonTheme.colorScheme?.primary),
|
||||||
buildCol("buttonTheme.colorScheme.secondary", Theme.of(context).buttonTheme.colorScheme?.secondary),
|
buildCol("buttonTheme.colorScheme.secondary", Theme.of(context).buttonTheme.colorScheme?.secondary),
|
||||||
buildCol("cardTheme.color", Theme.of(context).cardTheme.color),
|
buildCol("cardTheme.color", Theme.of(context).cardTheme.color),
|
||||||
|
@@ -60,7 +60,7 @@ class _DebugMainPageState extends State<DebugMainPage> {
|
|||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
||||||
],
|
],
|
||||||
style: ButtonStyle(
|
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),
|
visualDensity: VisualDensity(horizontal: -3, vertical: -3),
|
||||||
),
|
),
|
||||||
selected: <DebugMainPageSubPage>{_subPage},
|
selected: <DebugMainPageSubPage>{_subPage},
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/scn_message.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_hive.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class DebugPersistencePage extends StatefulWidget {
|
class DebugPersistencePage extends StatefulWidget {
|
||||||
@override
|
@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<SCNMessage>('scn-message-cache'), 'scn-message-cache'),
|
||||||
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
|
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
|
||||||
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
|
_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,63 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.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 Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
|
} 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");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
class DebugRequestViewPage extends StatelessWidget {
|
class DebugRequestViewPage extends StatefulWidget {
|
||||||
final SCNRequest request;
|
final SCNRequest request;
|
||||||
|
|
||||||
DebugRequestViewPage({required this.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SCNScaffold(
|
return SCNScaffold(
|
||||||
@@ -23,22 +33,22 @@ class DebugRequestViewPage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
...buildRow(context, "Name", request.name),
|
...buildRow(context, "name", "Name", widget.request.name),
|
||||||
...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()),
|
...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()),
|
||||||
...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()),
|
...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()),
|
||||||
...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()),
|
...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()),
|
||||||
Divider(),
|
Divider(),
|
||||||
...buildRow(context, "Method", request.method),
|
...buildRow(context, "method", "Method", widget.request.method),
|
||||||
...buildRow(context, "URL", request.url),
|
...buildRow(context, "url", "URL", widget.request.url, mono: true),
|
||||||
if (request.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
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 (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody),
|
if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true),
|
||||||
Divider(),
|
Divider(),
|
||||||
if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()),
|
if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()),
|
||||||
if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody),
|
if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true),
|
||||||
if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
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: true),
|
||||||
Divider(),
|
Divider(),
|
||||||
if (request.error != '') ...buildRow(context, "Error", request.error),
|
if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true),
|
||||||
if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace),
|
if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -46,7 +56,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 [
|
return [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0),
|
||||||
@@ -64,19 +86,46 @@ class DebugRequestViewPage extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
icon: FontAwesomeIcons.copy,
|
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(
|
Card.filled(
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
|
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
|
||||||
child: SelectableText(
|
child: (isMono || isJson)
|
||||||
value,
|
? SingleChildScrollView(
|
||||||
minLines: 1,
|
scrollDirection: Axis.horizontal,
|
||||||
maxLines: 10,
|
child: SelectableText(
|
||||||
),
|
value,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 10,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SelectableText(
|
||||||
|
value,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@@ -72,6 +72,9 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
|
|
||||||
_channels = SCNDataCache().getChannelMap();
|
_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);
|
_pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
|
||||||
|
|
||||||
_backgroundRefresh(true);
|
_backgroundRefresh(true);
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||||
import 'package:xid/xid.dart';
|
import 'package:xid/xid.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
part 'application_log.g.dart';
|
part 'application_log.g.dart';
|
||||||
|
|
||||||
@@ -76,6 +81,61 @@ class ApplicationLog {
|
|||||||
trace: trace?.toString() ?? '',
|
trace: trace?.toString() ?? '',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void writeRawFailure(String message, Map<String, dynamic> extraData) async {
|
||||||
|
try {
|
||||||
|
await Globals().init();
|
||||||
|
|
||||||
|
final fn = path.join(Globals().rawFailureLogsDir.path, 'failure-${DateTime.now().toIso8601String()}.log');
|
||||||
|
|
||||||
|
var txt = "[TEXT]\n${message}\n\n";
|
||||||
|
for (var k in extraData.keys) {
|
||||||
|
txt += "[${k}]\n${_debugToStr(extraData[k])}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
await File(fn).writeAsString(txt);
|
||||||
|
|
||||||
|
ApplicationLog.debug("Wrote raw failure log to '${fn}' ('${message}')");
|
||||||
|
} catch (e) {
|
||||||
|
print("Failed to <writeRawFailure>: ${e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _debugToStr(dynamic v) {
|
||||||
|
if (v is String) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (v is StackTrace) {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final enc = new JsonEncoder.withIndent(" ");
|
||||||
|
return enc.convert(v);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return jsonEncode(v);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return v.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return "${v}";
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<[!]FAILED_TO_PRINT_OBJECT>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 103)
|
@HiveType(typeId: 103)
|
||||||
|
@@ -2,7 +2,9 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class Globals {
|
class Globals {
|
||||||
static final Globals _singleton = Globals._internal();
|
static final Globals _singleton = Globals._internal();
|
||||||
@@ -26,6 +28,9 @@ class Globals {
|
|||||||
|
|
||||||
late SharedPreferences sharedPrefs;
|
late SharedPreferences sharedPrefs;
|
||||||
|
|
||||||
|
late Directory appDocumentsDir;
|
||||||
|
late Directory rawFailureLogsDir;
|
||||||
|
|
||||||
bool get isInitialized => _initialized;
|
bool get isInitialized => _initialized;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
@@ -61,6 +66,11 @@ class Globals {
|
|||||||
|
|
||||||
this.sharedPrefs = await SharedPreferences.getInstance();
|
this.sharedPrefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
this.appDocumentsDir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
|
this.rawFailureLogsDir = Directory(path.join(Globals().appDocumentsDir.path, "rawlogs"));
|
||||||
|
await this.rawFailureLogsDir.create(recursive: true);
|
||||||
|
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
flutter/lib/utils/dialogs.dart
Normal file
29
flutter/lib/utils/dialogs.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class UIDialogs {
|
||||||
|
static Future<String?> showTextInput(BuildContext context, String title, String hintText) {
|
||||||
|
var _textFieldController = TextEditingController();
|
||||||
|
|
||||||
|
return showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
controller: _textFieldController,
|
||||||
|
decoration: InputDecoration(hintText: hintText),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_textFieldController.text),
|
||||||
|
child: Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -72,7 +72,7 @@ class UI {
|
|||||||
splashColor: Theme.of(context).splashColor,
|
splashColor: Theme.of(context).splashColor,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -9,6 +9,7 @@ import device_info_plus
|
|||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import mobile_scanner
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
@@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_launcher_icons: "^0.13.1"
|
flutter_launcher_icons: ^0.14.3
|
||||||
|
|
||||||
font_awesome_flutter: '>= 4.7.0'
|
font_awesome_flutter: '>= 4.7.0'
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
@@ -21,21 +21,23 @@ dependencies:
|
|||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
infinite_scroll_pagination: ^4.0.0
|
infinite_scroll_pagination: ^4.0.0
|
||||||
intl: ^0.19.0
|
intl: ^0.20.2
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
package_info_plus: ^8.0.0
|
package_info_plus: ^8.0.0
|
||||||
xid: ^1.2.1
|
xid: ^1.2.1
|
||||||
flutter_lazy_indexed_stack: ^0.0.6
|
flutter_lazy_indexed_stack: ^0.0.6
|
||||||
firebase_core: ^2.32.0
|
firebase_core: ^3.13.0
|
||||||
firebase_messaging: ^14.9.4
|
firebase_messaging: ^15.2.5
|
||||||
device_info_plus: ^10.1.0
|
device_info_plus: ^11.3.0
|
||||||
toastification: ^2.0.0
|
toastification: ^3.0.1
|
||||||
uuid: ^4.4.0
|
uuid: ^4.4.0
|
||||||
share_plus: ^9.0.0
|
share_plus: ^10.1.4
|
||||||
flutter_local_notifications: ^17.1.2
|
flutter_local_notifications: ^17.2.3
|
||||||
|
|
||||||
|
|
||||||
|
path: any
|
||||||
|
mobile_scanner: ^6.0.1
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
font_awesome_flutter:
|
font_awesome_flutter:
|
||||||
path: deps/font_awesome_flutter
|
path: deps/font_awesome_flutter
|
||||||
@@ -45,7 +47,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^4.0.0
|
flutter_lints: ^5.0.0
|
||||||
hive_generator: ^2.0.1
|
hive_generator: ^2.0.1
|
||||||
build_runner: ^2.1.4
|
build_runner: ^2.1.4
|
||||||
|
|
||||||
|
@@ -411,7 +411,6 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo
|
|||||||
type query struct {
|
type query struct {
|
||||||
PageSize *int `json:"page_size" form:"page_size"`
|
PageSize *int `json:"page_size" form:"page_size"`
|
||||||
NextPageToken *string `json:"next_page_token" form:"next_page_token"`
|
NextPageToken *string `json:"next_page_token" form:"next_page_token"`
|
||||||
Filter *string `json:"filter" form:"filter"`
|
|
||||||
Trimmed *bool `json:"trimmed" form:"trimmed"`
|
Trimmed *bool `json:"trimmed" form:"trimmed"`
|
||||||
}
|
}
|
||||||
type response struct {
|
type response struct {
|
||||||
|
@@ -73,7 +73,7 @@ var configLocHost = func() Config {
|
|||||||
Journal: "WAL",
|
Journal: "WAL",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -86,7 +86,7 @@ var configLocHost = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -99,7 +99,7 @@ var configLocHost = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -145,7 +145,7 @@ var configLocDocker = func() Config {
|
|||||||
Journal: "WAL",
|
Journal: "WAL",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -158,7 +158,7 @@ var configLocDocker = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -171,7 +171,7 @@ var configLocDocker = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -216,7 +216,7 @@ var configDev = func() Config {
|
|||||||
Journal: "WAL",
|
Journal: "WAL",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -229,7 +229,7 @@ var configDev = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -242,7 +242,7 @@ var configDev = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -288,7 +288,7 @@ var configStag = func() Config {
|
|||||||
Journal: "WAL",
|
Journal: "WAL",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -301,7 +301,7 @@ var configStag = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -314,7 +314,7 @@ var configStag = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -360,7 +360,7 @@ var configProd = func() Config {
|
|||||||
Journal: "WAL",
|
Journal: "WAL",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -373,7 +373,7 @@ var configProd = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
@@ -386,7 +386,7 @@ var configProd = func() Config {
|
|||||||
Journal: "DELETE",
|
Journal: "DELETE",
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
CheckForeignKeys: false,
|
CheckForeignKeys: false,
|
||||||
SingleConn: true,
|
SingleConn: false,
|
||||||
MaxOpenConns: 5,
|
MaxOpenConns: 5,
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
ConnMaxLifetime: 60 * time.Minute,
|
ConnMaxLifetime: 60 * time.Minute,
|
||||||
|
@@ -23,6 +23,7 @@ type ChannelPreview struct {
|
|||||||
InternalName string `json:"internal_name"`
|
InternalName string `json:"internal_name"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
DescriptionName *string `json:"description_name"`
|
DescriptionName *string `json:"description_name"`
|
||||||
|
MessagesSent int `json:"messages_sent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
|
func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
|
||||||
@@ -39,5 +40,6 @@ func (c Channel) Preview() ChannelPreview {
|
|||||||
InternalName: c.InternalName,
|
InternalName: c.InternalName,
|
||||||
DisplayName: c.DisplayName,
|
DisplayName: c.DisplayName,
|
||||||
DescriptionName: c.DescriptionName,
|
DescriptionName: c.DescriptionName,
|
||||||
|
MessagesSent: c.MessagesSent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ type User struct {
|
|||||||
TimestampLastRead *SCNTime `db:"timestamp_lastread" json:"timestamp_lastread"`
|
TimestampLastRead *SCNTime `db:"timestamp_lastread" json:"timestamp_lastread"`
|
||||||
TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
|
TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
|
||||||
MessagesSent int `db:"messages_sent" json:"messages_sent"`
|
MessagesSent int `db:"messages_sent" json:"messages_sent"`
|
||||||
QuotaUsed int `db:"quota_used" json:"quota_used"`
|
QuotaUsed int `db:"quota_used" json:"-"`
|
||||||
QuotaUsedDay *string `db:"quota_used_day" json:"-"`
|
QuotaUsedDay *string `db:"quota_used_day" json:"-"`
|
||||||
IsPro bool `db:"is_pro" json:"is_pro"`
|
IsPro bool `db:"is_pro" json:"is_pro"`
|
||||||
ProToken *string `db:"pro_token" json:"-"`
|
ProToken *string `db:"pro_token" json:"-"`
|
||||||
@@ -22,6 +22,7 @@ type User struct {
|
|||||||
type UserExtra struct {
|
type UserExtra struct {
|
||||||
QuotaRemaining int `json:"quota_remaining"`
|
QuotaRemaining int `json:"quota_remaining"`
|
||||||
QuotaPerDay int `json:"quota_max"`
|
QuotaPerDay int `json:"quota_max"`
|
||||||
|
QuotaUsed int `json:"quota_used"`
|
||||||
DefaultChannel string `json:"default_channel"`
|
DefaultChannel string `json:"default_channel"`
|
||||||
MaxBodySize int `json:"max_body_size"`
|
MaxBodySize int `json:"max_body_size"`
|
||||||
MaxTitleLength int `json:"max_title_length"`
|
MaxTitleLength int `json:"max_title_length"`
|
||||||
@@ -58,6 +59,7 @@ func (u User) WithClients(clients []Client, ak string, sk string, rk string) Use
|
|||||||
func (u *User) PreMarshal() User {
|
func (u *User) PreMarshal() User {
|
||||||
u.UserExtra = UserExtra{
|
u.UserExtra = UserExtra{
|
||||||
QuotaPerDay: u.QuotaPerDay(),
|
QuotaPerDay: u.QuotaPerDay(),
|
||||||
|
QuotaUsed: u.QuotaUsedToday(),
|
||||||
QuotaRemaining: u.QuotaRemainingToday(),
|
QuotaRemaining: u.QuotaRemainingToday(),
|
||||||
DefaultChannel: u.DefaultChannel(),
|
DefaultChannel: u.DefaultChannel(),
|
||||||
MaxBodySize: u.MaxContentLength(),
|
MaxBodySize: u.MaxContentLength(),
|
||||||
|
Reference in New Issue
Block a user