Compare commits
No commits in common. "master" and "develop" have entirely different histories.
@ -3,12 +3,6 @@
|
|||||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||||
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||||
|
|
||||||
# Configurable with a few commit messages:
|
|
||||||
# - [skip-tests] Skip the test stage
|
|
||||||
# - [skip-deployment] Skip the deployment stage
|
|
||||||
# - [skip-ci] Skip all stages (the whole ci/cd)
|
|
||||||
#
|
|
||||||
|
|
||||||
name: Build Docker and Deploy
|
name: Build Docker and Deploy
|
||||||
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }}
|
||||||
|
|
||||||
@ -22,9 +16,6 @@ jobs:
|
|||||||
build_server:
|
build_server:
|
||||||
name: Build Docker Container
|
name: Build Docker Container
|
||||||
runs-on: bfb-cicd-latest
|
runs-on: bfb-cicd-latest
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip-deployment]')
|
|
||||||
steps:
|
steps:
|
||||||
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
|
- run: echo -n "${{ secrets.DOCKER_REG_PASS }}" | docker login registry.blackforestbytes.com -u docker --password-stdin
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
@ -36,9 +27,6 @@ jobs:
|
|||||||
test_server:
|
test_server:
|
||||||
name: Run Unit-Tests
|
name: Run Unit-Tests
|
||||||
runs-on: bfb-cicd-latest
|
runs-on: bfb-cicd-latest
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip-tests]')
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
@ -80,12 +68,6 @@ jobs:
|
|||||||
name: Deploy to Server
|
name: Deploy to Server
|
||||||
needs: [build_server, test_server]
|
needs: [build_server, test_server]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
|
||||||
!cancelled() &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip-ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip-deployment]') &&
|
|
||||||
needs.build_server.result == 'success' &&
|
|
||||||
(needs.test_server.result == 'skipped' || needs.test_server.result == 'success')
|
|
||||||
steps:
|
steps:
|
||||||
- name: Execute deploy on remote (via ssh)
|
- name: Execute deploy on remote (via ssh)
|
||||||
uses: appleboy/ssh-action@v1.0.0
|
uses: appleboy/ssh-action@v1.0.0
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
.aider*
|
|
6
android/.idea/AndroidProjectSystem.xml
generated
6
android/.idea/AndroidProjectSystem.xml
generated
@ -1,6 +0,0 @@
|
|||||||
<?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
Normal file
263
android/.idea/other.xml
generated
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<?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
17
android/.idea/runConfigurations.xml
generated
@ -1,17 +0,0 @@
|
|||||||
<?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,7 +30,6 @@ android {
|
|||||||
targetCompatibility 1.8
|
targetCompatibility 1.8
|
||||||
sourceCompatibility 1.8
|
sourceCompatibility 1.8
|
||||||
}
|
}
|
||||||
namespace 'com.blackforestbytes.simplecloudnotifier'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?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:8.9.1'
|
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||||
classpath 'com.google.gms:google-services:4.3.10'
|
classpath 'com.google.gms:google-services:4.3.4'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,3 @@ 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-8.11.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||||
|
4
flutter/.gitignore
vendored
4
flutter/.gitignore
vendored
@ -5,8 +5,6 @@
|
|||||||
firepit-log.txt
|
firepit-log.txt
|
||||||
flutter_jank_*
|
flutter_jank_*
|
||||||
|
|
||||||
_releases/*
|
|
||||||
|
|
||||||
|
|
||||||
#######################################################################################################################
|
#######################################################################################################################
|
||||||
|
|
||||||
@ -56,5 +54,3 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
/lib/git_stamp/
|
|
||||||
|
@ -8,29 +8,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# runs app locally (linux)
|
run:
|
||||||
run-linux: gen
|
flutter pub run build_runner build
|
||||||
dart run build_runner build
|
_JAVA_OPTIONS="" flutter run
|
||||||
_JAVA_OPTIONS="" flutter run -d linux
|
|
||||||
|
|
||||||
# runs app locally (web | not really supported)
|
run-android:
|
||||||
run-web: gen
|
|
||||||
dart run build_runner build
|
|
||||||
_JAVA_OPTIONS="" flutter run -d chrome
|
|
||||||
|
|
||||||
# runs on android device (must have network adb enabled teh correct IP)
|
|
||||||
run-android: gen
|
|
||||||
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
|
||||||
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
|
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
|
||||||
|
|
||||||
install-release: gen
|
install-release:
|
||||||
# Install on Pixel 7a
|
# Install on Pixel 7a
|
||||||
flutter build apk --release
|
flutter build apk --release
|
||||||
flutter run --release -d 35221JEHN07157
|
flutter run --release -d 35221JEHN07157
|
||||||
|
|
||||||
build-release: gen
|
build-release:
|
||||||
flutter build apk --release
|
flutter build apk --release
|
||||||
flutter build appbundle --release
|
flutter build appbundle --release
|
||||||
flutter build linux --release
|
flutter build linux --release
|
||||||
@ -42,13 +35,10 @@ fix:
|
|||||||
dart fix --apply
|
dart fix --apply
|
||||||
|
|
||||||
gen:
|
gen:
|
||||||
./_utils/inc_buildnum.sh
|
flutter pub run build_runner build
|
||||||
dart run build_runner build
|
|
||||||
dart run git_stamp git_stamp --build-type lite --limit 2
|
|
||||||
|
|
||||||
# run `make run` in another terminal (or another variant of flutter run)
|
|
||||||
autoreload:
|
autoreload:
|
||||||
@
|
@# run `make run` in another terminal (or another variant of flutter run)
|
||||||
@_utils/autoreload.sh
|
@_utils/autoreload.sh
|
||||||
|
|
||||||
icons:
|
icons:
|
||||||
@ -57,15 +47,3 @@ icons:
|
|||||||
clean:
|
clean:
|
||||||
cd android && ./gradlew clean
|
cd android && ./gradlew clean
|
||||||
flutter 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
|
|
||||||
|
|
||||||
aider:
|
|
||||||
aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only
|
|
@ -1,38 +1,31 @@
|
|||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [x] Message List
|
- [ ] Message List
|
||||||
* [x] CRUD
|
* [ ] CRUD
|
||||||
- [x] Message Big-View
|
- [ ] Message Big-View
|
||||||
- [x] Search/Filter Messages
|
- [ ] Search/Filter Messages
|
||||||
- [x] Channel List
|
- [ ] Channel List
|
||||||
* [x] Show subs
|
* [ ] Show subs
|
||||||
* [x] CRUD
|
* [ ] CRUD
|
||||||
* [x] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
* [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
||||||
- [x] Sub List
|
- [ ] Sub List
|
||||||
* [x] Sub/Unsub/Accept/Deny
|
* [ ] Sub/Unsub/Accept/Deny
|
||||||
- [x] Debug List (Show logs, requests)
|
- [ ] Debug List (Show logs, requests)
|
||||||
- [x] Key List
|
- [ ] Key List
|
||||||
* [x] CRUD
|
* [ ] CRUD
|
||||||
- [x] Auto R-only key for admin, use for QR+link+send
|
- [ ] Auto R-only key for admin, use for QR+link+send
|
||||||
- [ ] settings
|
- [ ] settings
|
||||||
- [?] notifications
|
- [ ] notifications
|
||||||
- [?] push navigation stack
|
- [ ] push navigation stack
|
||||||
- [/] read + migrate old SharedPrefs (or not? - who uses SCN even??)
|
- [ ] read + migrate old SharedPrefs (or not? - who uses SCN even??)
|
||||||
- [x] Account-Page
|
- [ ] Account-Page
|
||||||
- [x] Logout
|
- [ ] Logout
|
||||||
- [x] Send-page
|
- [ ] Send-page
|
||||||
|
|
||||||
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
|
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
|
||||||
|
|
||||||
- [x] fix time format (in message-list, in card, top right) - midnight is shown as "24:05" instead of "00:05" - thats weird
|
- [ ] fix time format (in message-list, in card, top right) - midnight is shown as "24:05" instead of "00:05" - thats weird
|
||||||
|
|
||||||
- [x] Add scrollbar
|
|
||||||
-> https://api.flutter.dev/flutter/material/Scrollbar-class.html
|
|
||||||
|
|
||||||
- [x] you cant unsubscribe from foreign channel without completely loosing subscription.
|
|
||||||
perhaps subscriptions should have two cofirmed bool (both must be true to receive messages): confirmed-owner && confirmed-subscriber
|
|
||||||
Then the subscriber can unconfirm his half - without loosing the owner confirmation
|
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
# shellcheck disable=SC2002 # disable useless-cat warning
|
||||||
|
|
||||||
set -o nounset # disallow usage of unset vars ( set -u )
|
set -o nounset # disallow usage of unset vars ( set -u )
|
||||||
#set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||||
#set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||||
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
||||||
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
||||||
|
|
||||||
@ -24,34 +24,23 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
pids="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )"
|
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
|
||||||
|
|
||||||
if [ -z "$pids" ]; then
|
if [ -z "$pid" ]; then
|
||||||
red "No [flutter run] process found - exiting"
|
red "No [flutter run] process found - exiting"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trap 'echo "reseived SIGNAL<EXIT> - exiting"; jobs -p | xargs kill ; exit 0' EXIT
|
trap 'echo "reseived SIGNAL<EXIT> - exiting"; exit 0' EXIT
|
||||||
trap 'echo "reseived SIGNAL<SIGINT> - exiting"; jobs -p | xargs kill ; exit 0' SIGINT
|
trap 'echo "reseived SIGNAL<SIGINT> - exiting"; exit 0' SIGINT
|
||||||
trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; jobs -p | xargs kill ; exit 0' SIGTERM
|
trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; exit 0' SIGTERM
|
||||||
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; jobs -p | xargs kill ; exit 0' SIGQUIT
|
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; exit 0' SIGQUIT
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
while IFS= read -r pid; do
|
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
||||||
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
|
||||||
done <<< "$pids"
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
while IFS= read -r pid; do
|
while true; do
|
||||||
{
|
find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid";
|
||||||
while true; do
|
yellow 'File list changed - restart';
|
||||||
find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid";
|
done
|
||||||
yellow 'File list changed - restart';
|
|
||||||
done
|
|
||||||
} &
|
|
||||||
done <<< "$pids"
|
|
||||||
|
|
||||||
wait # wait for all background jobs to finish
|
|
||||||
|
|
||||||
echo "DONE."
|
|
@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
|
||||||
|
|
||||||
set -o nounset # disallow usage of unset vars ( set -u )
|
|
||||||
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
|
||||||
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
|
||||||
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
|
||||||
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
|
||||||
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
cr=$'\n'
|
|
||||||
|
|
||||||
function black() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[30m$1\\x1B[0m"; else echo "$1"; fi }
|
|
||||||
function red() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[31m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function green() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[32m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function yellow(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[33m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function blue() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[34m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function purple(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[35m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function cyan() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[36m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
function white() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[37m$1\\x1B[0m"; else echo "$1"; fi; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
path_to_pubspec="$(dirname "$0")/../pubspec.yaml"
|
|
||||||
current_version=$(awk '/^version:/ {print $2}' $path_to_pubspec)
|
|
||||||
current_version_without_build=$(echo "$current_version" | sed 's/\+.*//')
|
|
||||||
|
|
||||||
gitcount="$(git log | grep "^commit" | wc -l | xargs)"
|
|
||||||
new_version="$current_version_without_build+$gitcount"
|
|
||||||
|
|
||||||
echo "Setting pubspec.yaml version $current_version to $new_version"
|
|
||||||
|
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
||||||
# macOS sed (requires a space after -i)
|
|
||||||
sed -i '' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec
|
|
||||||
else
|
|
||||||
# GNU sed (requires no space after -i)
|
|
||||||
sed -i'' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec
|
|
||||||
fi
|
|
4
flutter/android/.gitignore
vendored
4
flutter/android/.gitignore
vendored
@ -11,7 +11,3 @@ GeneratedPluginRegistrant.java
|
|||||||
key.properties
|
key.properties
|
||||||
**/*.keystore
|
**/*.keystore
|
||||||
**/*.jks
|
**/*.jks
|
||||||
|
|
||||||
build/
|
|
||||||
|
|
||||||
app/.cxx/
|
|
@ -34,7 +34,7 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
android {
|
android {
|
||||||
namespace "com.blackforestbytes.simplecloudnotifier"
|
namespace "com.blackforestbytes.simplecloudnotifier"
|
||||||
compileSdkVersion flutter.compileSdkVersion
|
compileSdkVersion flutter.compileSdkVersion
|
||||||
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)
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
|
@ -1,3 +1,16 @@
|
|||||||
|
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
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -23,8 +23,7 @@ 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 "8.9.1" apply false
|
id "com.android.application" version "7.3.0" 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
|
||||||
|
@ -5,7 +5,6 @@ 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/send_message_response.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/sender_name_statistics.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';
|
||||||
@ -32,31 +31,26 @@ enum ChannelSelector {
|
|||||||
class MessageFilter {
|
class MessageFilter {
|
||||||
List<String>? channelIDs;
|
List<String>? channelIDs;
|
||||||
List<String>? searchFilter;
|
List<String>? searchFilter;
|
||||||
List<String>? plainSearchFilter;
|
|
||||||
List<String>? senderNames;
|
List<String>? senderNames;
|
||||||
List<String>? usedKeys;
|
List<String>? usedKeys;
|
||||||
List<int>? priority;
|
List<int>? priority;
|
||||||
DateTime? timeBefore;
|
DateTime? timeBefore;
|
||||||
DateTime? timeAfter;
|
DateTime? timeAfter;
|
||||||
bool? hasSenderName;
|
bool? hasSenderName;
|
||||||
List<String>? senderUserID;
|
|
||||||
|
|
||||||
MessageFilter({
|
MessageFilter({
|
||||||
this.channelIDs,
|
this.channelIDs,
|
||||||
this.searchFilter,
|
this.searchFilter,
|
||||||
this.plainSearchFilter,
|
|
||||||
this.senderNames,
|
this.senderNames,
|
||||||
this.usedKeys,
|
this.usedKeys,
|
||||||
this.priority,
|
this.priority,
|
||||||
this.timeBefore,
|
this.timeBefore,
|
||||||
this.timeAfter,
|
this.timeAfter,
|
||||||
this.senderUserID,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
static const String _base = 'https://simplecloudnotifier.de';
|
static const String _base = 'https://simplecloudnotifier.de/api/v2';
|
||||||
static const String _prefix = '/api/v2';
|
|
||||||
|
|
||||||
static Future<T> _request<T>({
|
static Future<T> _request<T>({
|
||||||
required String name,
|
required String name,
|
||||||
@ -67,11 +61,10 @@ class APIClient {
|
|||||||
dynamic jsonBody,
|
dynamic jsonBody,
|
||||||
String? authToken,
|
String? authToken,
|
||||||
Map<String, String>? header,
|
Map<String, String>? header,
|
||||||
bool? nonAPI,
|
|
||||||
}) async {
|
}) async {
|
||||||
final t0 = DateTime.now();
|
final t0 = DateTime.now();
|
||||||
|
|
||||||
final uri = Uri.parse('$_base${(nonAPI ?? false) ? '' : _prefix}/$relURL').replace(queryParameters: query ?? {});
|
final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {});
|
||||||
|
|
||||||
final req = http.Request(method, uri);
|
final req = http.Request(method, uri);
|
||||||
|
|
||||||
@ -169,30 +162,6 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<User> updateUser(TokenSource auth, String uid, {String? username, String? proToken}) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'updateUser',
|
|
||||||
method: 'PATCH',
|
|
||||||
relURL: 'users/$uid',
|
|
||||||
jsonBody: {
|
|
||||||
if (username != null) 'username': username,
|
|
||||||
if (proToken != null) 'pro_token': proToken,
|
|
||||||
},
|
|
||||||
fn: User.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<User> deleteUser(TokenSource auth, String uid) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'deleteUser',
|
|
||||||
method: 'DELETE',
|
|
||||||
relURL: 'users/$uid',
|
|
||||||
fn: User.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
||||||
@ -210,16 +179,16 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Client> updateClient(TokenSource auth, String clientID, {String? fcmToken, String? agentModel, String? name, String? agentVersion}) async {
|
static Future<Client> updateClient(TokenSource auth, String clientID, String fcmToken, String agentModel, String? name, String agentVersion) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'updateClient',
|
name: 'updateClient',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
relURL: 'users/${auth.getUserID()}/clients/$clientID',
|
relURL: 'users/${auth.getUserID()}/clients/$clientID',
|
||||||
jsonBody: {
|
jsonBody: {
|
||||||
if (fcmToken != null) 'fcm_token': fcmToken,
|
'fcm_token': fcmToken,
|
||||||
if (agentModel != null) 'agent_model': agentModel,
|
'agent_model': agentModel,
|
||||||
if (agentVersion != null) 'agent_version': agentVersion,
|
'agent_version': agentVersion,
|
||||||
if (name != null) 'name': name,
|
'name': name,
|
||||||
},
|
},
|
||||||
fn: Client.fromJson,
|
fn: Client.fromJson,
|
||||||
authToken: auth.getToken(),
|
authToken: auth.getToken(),
|
||||||
@ -283,7 +252,7 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter, bool? includeNonSuscribed}) async {
|
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter}) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getMessageList',
|
name: 'getMessageList',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -292,7 +261,6 @@ class APIClient {
|
|||||||
'next_page_token': [pageToken],
|
'next_page_token': [pageToken],
|
||||||
if (pageSize != null) 'page_size': [pageSize.toString()],
|
if (pageSize != null) 'page_size': [pageSize.toString()],
|
||||||
if (filter?.searchFilter != null) 'search': filter!.searchFilter!,
|
if (filter?.searchFilter != null) 'search': filter!.searchFilter!,
|
||||||
if (filter?.plainSearchFilter != null) 'string_search': filter!.plainSearchFilter!,
|
|
||||||
if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!,
|
if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!,
|
||||||
if (filter?.senderNames != null) 'sender': filter!.senderNames!,
|
if (filter?.senderNames != null) 'sender': filter!.senderNames!,
|
||||||
if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()],
|
if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()],
|
||||||
@ -300,8 +268,6 @@ class APIClient {
|
|||||||
if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()],
|
if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()],
|
||||||
if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(),
|
if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(),
|
||||||
if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!,
|
if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!,
|
||||||
if (filter?.senderUserID != null) 'sender_user_id': filter!.senderUserID!,
|
|
||||||
if (includeNonSuscribed ?? false) 'subscription_status': ['all'],
|
|
||||||
},
|
},
|
||||||
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
|
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
|
||||||
authToken: auth.getToken(),
|
authToken: auth.getToken(),
|
||||||
@ -332,26 +298,11 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Subscription> getSubscription(TokenSource auth, String subscriptionID) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'getSubscription',
|
|
||||||
method: 'GET',
|
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subscriptionID}',
|
|
||||||
fn: Subscription.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
|
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getSubscriptionList',
|
name: 'getSubscriptionList',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||||
query: {
|
|
||||||
'direction': ['both'],
|
|
||||||
'confirmation': ['all'],
|
|
||||||
'external': ['all'],
|
|
||||||
},
|
|
||||||
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
|
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
|
||||||
authToken: auth.getToken(),
|
authToken: auth.getToken(),
|
||||||
);
|
);
|
||||||
@ -415,9 +366,9 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<KeyTokenPreview> getKeyTokenPreviewByID(TokenSource auth, String kid) async {
|
static Future<KeyTokenPreview> getKeyTokenPreview(TokenSource auth, String kid) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getKeyTokenPreviewByID',
|
name: 'getKeyTokenPreview',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
relURL: 'preview/keys/$kid',
|
relURL: 'preview/keys/$kid',
|
||||||
fn: KeyTokenPreview.fromJson,
|
fn: KeyTokenPreview.fromJson,
|
||||||
@ -425,16 +376,6 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<KeyTokenPreview> getKeyTokenPreviewByToken(TokenSource auth, String tok) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'getKeyTokenPreviewByToken',
|
|
||||||
method: 'GET',
|
|
||||||
relURL: 'preview/keys/$tok',
|
|
||||||
fn: KeyTokenPreview.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getCurrentKeyToken',
|
name: 'getCurrentKeyToken',
|
||||||
@ -445,48 +386,6 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> deleteKeyToken(AppAuth acc, String keytokenID) {
|
|
||||||
return _request(
|
|
||||||
name: 'deleteKeyToken',
|
|
||||||
method: 'DELETE',
|
|
||||||
relURL: 'users/${acc.getUserID()}/keys/${keytokenID}',
|
|
||||||
fn: (_) => null,
|
|
||||||
authToken: acc.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'updateKeyToken',
|
|
||||||
method: 'PATCH',
|
|
||||||
relURL: 'users/${auth.getUserID()}/keys/${kid}',
|
|
||||||
jsonBody: {
|
|
||||||
if (name != null) 'name': name,
|
|
||||||
if (allChannels != null) 'all_channels': allChannels,
|
|
||||||
if (channels != null) 'channels': channels,
|
|
||||||
if (permissions != null) 'permissions': permissions,
|
|
||||||
},
|
|
||||||
fn: KeyToken.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<KeyTokenWithToken> createKeyToken(TokenSource auth, String name, String perm, bool allChannels, {List<String>? channels}) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'createKeyToken',
|
|
||||||
method: 'POST',
|
|
||||||
relURL: 'users/${auth.getUserID()}/keys',
|
|
||||||
jsonBody: {
|
|
||||||
'name': name,
|
|
||||||
'permissions': perm,
|
|
||||||
'all_channels': allChannels,
|
|
||||||
if (channels != null) 'channels': channels,
|
|
||||||
},
|
|
||||||
fn: KeyTokenWithToken.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
|
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getSenderNameList',
|
name: 'getSenderNameList',
|
||||||
@ -497,14 +396,11 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID, {String? subscribeKey}) async {
|
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'subscribeToChannelbyID',
|
name: 'subscribeToChannelbyID',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||||
query: {
|
|
||||||
if (subscribeKey != null) 'chan_subscribe_key': [subscribeKey],
|
|
||||||
},
|
|
||||||
jsonBody: {
|
jsonBody: {
|
||||||
'channel_id': channelID,
|
'channel_id': channelID,
|
||||||
},
|
},
|
||||||
@ -548,52 +444,4 @@ class APIClient {
|
|||||||
authToken: auth.getToken(),
|
authToken: auth.getToken(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Subscription> activateSubscription(TokenSource auth, String channelID, String subID) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'activateSubscription',
|
|
||||||
method: 'PATCH',
|
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
|
||||||
jsonBody: {
|
|
||||||
'active': true,
|
|
||||||
},
|
|
||||||
fn: Subscription.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<Subscription> deactivateSubscription(TokenSource auth, String channelID, String subID) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'deactivateSubscription',
|
|
||||||
method: 'PATCH',
|
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
|
|
||||||
jsonBody: {
|
|
||||||
'active': false,
|
|
||||||
},
|
|
||||||
fn: Subscription.fromJson,
|
|
||||||
authToken: auth.getToken(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<SendMessageResponse> sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async {
|
|
||||||
return await _request(
|
|
||||||
name: 'sendMessage',
|
|
||||||
method: 'POST',
|
|
||||||
relURL: '/send',
|
|
||||||
nonAPI: true,
|
|
||||||
jsonBody: {
|
|
||||||
'user_id': userid,
|
|
||||||
'key': keytoken,
|
|
||||||
'title': text,
|
|
||||||
if (channel != null) 'channel': channel,
|
|
||||||
if (content != null) 'content': content,
|
|
||||||
if (priority != null) 'priority': priority,
|
|
||||||
if (messageID != null) 'msg_id': messageID,
|
|
||||||
if (timestamp != null) 'timestamp': (timestamp.microsecondsSinceEpoch / 1000).toInt(),
|
|
||||||
if (senderName != null) 'sender_name': senderName,
|
|
||||||
},
|
|
||||||
fn: SendMessageResponse.fromJson,
|
|
||||||
authToken: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
enum BadgeMode { error, warn, info }
|
|
||||||
|
|
||||||
class BadgeDisplay extends StatelessWidget {
|
|
||||||
final String text;
|
|
||||||
final BadgeMode mode;
|
|
||||||
final IconData? icon;
|
|
||||||
|
|
||||||
const BadgeDisplay({
|
|
||||||
Key? key,
|
|
||||||
required this.text,
|
|
||||||
required this.mode,
|
|
||||||
required this.icon,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var col = Colors.grey;
|
|
||||||
var colFG = Colors.black;
|
|
||||||
|
|
||||||
if (mode == BadgeMode.error) col = Colors.red;
|
|
||||||
if (mode == BadgeMode.warn) col = Colors.orange;
|
|
||||||
if (mode == BadgeMode.info) col = Colors.blue;
|
|
||||||
|
|
||||||
if (mode == BadgeMode.error) colFG = Colors.red[900]!;
|
|
||||||
if (mode == BadgeMode.warn) colFG = Colors.black;
|
|
||||||
if (mode == BadgeMode.info) colFG = Colors.black;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: col[100],
|
|
||||||
border: Border.all(color: col[300]!),
|
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (icon != null) Icon(icon!, color: colFG, size: 16.0),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(color: colFG, fontSize: 14.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
|
|
||||||
class ErrorDisplay extends StatelessWidget {
|
|
||||||
final String errorMessage;
|
|
||||||
|
|
||||||
const ErrorDisplay({Key? key, required this.errorMessage}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(32.0),
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.red[100],
|
|
||||||
border: Border.all(color: Colors.red[300]!),
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.triangleExclamation, color: Colors.red, size: 48.0),
|
|
||||||
const SizedBox(height: 16.0),
|
|
||||||
Text(
|
|
||||||
errorMessage,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(color: Colors.red[900], fontSize: 16.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import 'package:simplecloudnotifier/components/layout/app_bar_filter_dialog.dart
|
|||||||
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||||
@ -64,7 +64,7 @@ class _SCNAppBarState extends State<SCNAppBar> {
|
|||||||
builder: (context, appTheme, child) => IconButton(
|
builder: (context, appTheme, child) => IconButton(
|
||||||
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
|
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
|
||||||
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
|
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
|
||||||
onPressed: AppTheme().switchDarkMode,
|
onPressed: appTheme.switchDarkMode,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_channel.dart';
|
import 'package:simplecloudnotifier/components/modals/filter_modal_channel.dart';
|
||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_keytoken.dart';
|
import 'package:simplecloudnotifier/components/modals/filter_modal_keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_priority.dart';
|
import 'package:simplecloudnotifier/components/modals/filter_modal_priority.dart';
|
||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_searchplain.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_sendername.dart';
|
import 'package:simplecloudnotifier/components/modals/filter_modal_sendername.dart';
|
||||||
import 'package:simplecloudnotifier/components/modals/filter_modal_time.dart';
|
import 'package:simplecloudnotifier/components/modals/filter_modal_time.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
@ -17,9 +16,7 @@ class AppBarFilterDialog extends StatefulWidget {
|
|||||||
class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
|
class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
|
||||||
double _height = 0;
|
double _height = 0;
|
||||||
|
|
||||||
static const int _itemCount = 7;
|
double _targetHeight = 4 + (48 * 6) + (16 * 5) + 4;
|
||||||
|
|
||||||
static const double _targetHeight = 4 + (48 * _itemCount) + (16 * (_itemCount - 1)) + 4;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -120,6 +117,6 @@ class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showPlainSearchModal(BuildContext context) {
|
void _showPlainSearchModal(BuildContext context) {
|
||||||
showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
|
//TODO showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,9 @@ class SCNScaffold extends StatelessWidget {
|
|||||||
this.showSearch = true,
|
this.showSearch = true,
|
||||||
this.showShare = false,
|
this.showShare = false,
|
||||||
this.onShare = null,
|
this.onShare = null,
|
||||||
this.floatingActionButton = null,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Widget? floatingActionButton;
|
|
||||||
final String? title;
|
final String? title;
|
||||||
final bool showThemeSwitch;
|
final bool showThemeSwitch;
|
||||||
final bool showSearch;
|
final bool showSearch;
|
||||||
@ -32,7 +30,6 @@ class SCNScaffold extends StatelessWidget {
|
|||||||
onShare: onShare ?? () {},
|
onShare: onShare ?? () {},
|
||||||
),
|
),
|
||||||
body: child,
|
body: child,
|
||||||
floatingActionButton: floatingActionButton,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
class FilterModalChannel extends StatefulWidget {
|
class FilterModalChannel extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -17,12 +15,13 @@ class FilterModalChannel extends StatefulWidget {
|
|||||||
class _FilterModalChannelState extends State<FilterModalChannel> {
|
class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
ImmediateFuture<List<Channel>> _futureChannels = ImmediateFuture.ofPending();
|
late ImmediateFuture<List<Channel>>? _futureChannels;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_futureChannels = null;
|
||||||
_futureChannels = ImmediateFuture.ofFuture(() async {
|
_futureChannels = ImmediateFuture.ofFuture(() async {
|
||||||
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');
|
||||||
@ -50,39 +49,45 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: FutureBuilder(
|
child: () {
|
||||||
future: _futureChannels.future,
|
if (_futureChannels == null) {
|
||||||
builder: ((context, snapshot) {
|
return Center(child: CircularProgressIndicator());
|
||||||
if (_futureChannels.value != null) {
|
}
|
||||||
return _buildList(context, _futureChannels.value!);
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
return FutureBuilder(
|
||||||
return Center(child: CircularProgressIndicator());
|
future: _futureChannels!.future,
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
builder: ((context, snapshot) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
if (_futureChannels?.value != null) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
return _buildList(context, _futureChannels!.value!);
|
||||||
return _buildList(context, snapshot.data!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
} else {
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
}
|
return _buildList(context, snapshot.data!);
|
||||||
}),
|
} else {
|
||||||
),
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
child: const Text('Apply'),
|
child: const Text('Apply'),
|
||||||
onPressed: _onOkay,
|
onPressed: () {
|
||||||
|
onOkay();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onOkay() {
|
void onOkay() {
|
||||||
Navi.popDialog(context);
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final chiplets = _selectedEntries
|
final chiplets = _selectedEntries
|
||||||
.map((e) => MessageFilterChiplet(
|
.map((e) => MessageFilterChiplet(
|
||||||
label: _futureChannels.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
label: _futureChannels?.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
||||||
value: e,
|
value: e,
|
||||||
type: MessageFilterChipletType.channel,
|
type: MessageFilterChipletType.channel,
|
||||||
))
|
))
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
class FilterModalKeytoken extends StatefulWidget {
|
class FilterModalKeytoken extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -17,12 +15,13 @@ class FilterModalKeytoken extends StatefulWidget {
|
|||||||
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
ImmediateFuture<List<KeyToken>> _futureKeyTokens = ImmediateFuture.ofPending();
|
late ImmediateFuture<List<KeyToken>>? _futureKeyTokens;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_futureKeyTokens = null;
|
||||||
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
||||||
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');
|
||||||
@ -50,22 +49,26 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: FutureBuilder(
|
child: () {
|
||||||
future: _futureKeyTokens.future,
|
if (_futureKeyTokens == null) {
|
||||||
builder: ((context, snapshot) {
|
return Center(child: CircularProgressIndicator());
|
||||||
if (_futureKeyTokens.value != null) {
|
}
|
||||||
return _buildList(context, _futureKeyTokens.value!);
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
return FutureBuilder(
|
||||||
return Center(child: CircularProgressIndicator());
|
future: _futureKeyTokens!.future,
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
builder: ((context, snapshot) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
if (_futureKeyTokens?.value != null) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
return _buildList(context, _futureKeyTokens!.value!);
|
||||||
return _buildList(context, snapshot.data!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
} else {
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
}
|
return _buildList(context, snapshot.data!);
|
||||||
}),
|
} else {
|
||||||
),
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -80,11 +83,11 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOkay() {
|
void onOkay() {
|
||||||
Navi.popDialog(context);
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final chiplets = _selectedEntries
|
final chiplets = _selectedEntries
|
||||||
.map((e) => MessageFilterChiplet(
|
.map((e) => MessageFilterChiplet(
|
||||||
label: _futureKeyTokens.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
label: _futureKeyTokens?.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
||||||
value: e,
|
value: e,
|
||||||
type: MessageFilterChipletType.sender,
|
type: MessageFilterChipletType.sender,
|
||||||
))
|
))
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
class FilterModalPriority extends StatefulWidget {
|
class FilterModalPriority extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -59,7 +58,7 @@ class _FilterModalPriorityState extends State<FilterModalPriority> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOkay() {
|
void onOkay() {
|
||||||
Navi.popDialog(context);
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final chiplets = _selectedEntries.map((e) => MessageFilterChiplet(label: _texts[e]?.$2 ?? '???', value: e, type: MessageFilterChipletType.priority)).toList();
|
final chiplets = _selectedEntries.map((e) => MessageFilterChiplet(label: _texts[e]?.$2 ?? '???', value: e, type: MessageFilterChipletType.priority)).toList();
|
||||||
|
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
class FilterModalSearchPlain extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_FilterModalSearchPlainState createState() => _FilterModalSearchPlainState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FilterModalSearchPlainState extends State<FilterModalSearchPlain> {
|
|
||||||
final _controller = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Search'),
|
|
||||||
content: Container(
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: _controller,
|
|
||||||
decoration: InputDecoration(hintText: "Search..."),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
|
||||||
child: const Text('Apply'),
|
|
||||||
onPressed: _onOkay,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onOkay() {
|
|
||||||
Navi.popDialog(context);
|
|
||||||
|
|
||||||
List<MessageFilterChiplet> chiplets = [];
|
|
||||||
if (_controller.text.isNotEmpty) {
|
|
||||||
chiplets.add(MessageFilterChiplet(
|
|
||||||
label: _controller.text,
|
|
||||||
value: _controller.text,
|
|
||||||
type: MessageFilterChipletType.plainSearch,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
AppEvents().notifyFilterListeners([MessageFilterChipletType.plainSearch], chiplets);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
@ -15,12 +14,13 @@ class FilterModalSendername extends StatefulWidget {
|
|||||||
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
ImmediateFuture<List<String>> _futureSenders = ImmediateFuture.ofPending();
|
late ImmediateFuture<List<String>>? _futureSenders;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_futureSenders = null;
|
||||||
_futureSenders = ImmediateFuture.ofFuture(() async {
|
_futureSenders = ImmediateFuture.ofFuture(() async {
|
||||||
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');
|
||||||
@ -48,34 +48,40 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: FutureBuilder(
|
child: () {
|
||||||
future: _futureSenders.future,
|
if (_futureSenders == null) {
|
||||||
builder: ((context, snapshot) {
|
return Center(child: CircularProgressIndicator());
|
||||||
if (_futureSenders.value != null) {
|
}
|
||||||
return _buildList(context, _futureSenders.value!);
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
return FutureBuilder(
|
||||||
return Center(child: CircularProgressIndicator());
|
future: _futureSenders!.future,
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
builder: ((context, snapshot) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
if (_futureSenders?.value != null) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
return _buildList(context, _futureSenders!.value!);
|
||||||
return _buildList(context, snapshot.data!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
} else {
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
return ErrorDisplay(errorMessage: 'Invalid future state');
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
}
|
return _buildList(context, snapshot.data!);
|
||||||
}),
|
} else {
|
||||||
),
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
child: const Text('Apply'),
|
child: const Text('Apply'),
|
||||||
onPressed: _onOkay,
|
onPressed: () {
|
||||||
|
onOkay();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onOkay() {
|
void onOkay() {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
final chiplets = _selectedEntries
|
final chiplets = _selectedEntries
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
class FilterModalTime extends StatefulWidget {
|
class FilterModalTime extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -37,7 +36,7 @@ class _FilterModalTimeState extends State<FilterModalTime> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onOkay() {
|
void onOkay() {
|
||||||
Navi.popDialog(context);
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,11 @@ import 'package:hive_flutter/hive_flutter.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/models/client.dart';
|
import 'package:simplecloudnotifier/models/client.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/nav_layout.dart';
|
import 'package:simplecloudnotifier/nav_layout.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||||
@ -51,7 +50,6 @@ void main() async {
|
|||||||
Hive.registerAdapter(SCNMessageAdapter());
|
Hive.registerAdapter(SCNMessageAdapter());
|
||||||
Hive.registerAdapter(ChannelAdapter());
|
Hive.registerAdapter(ChannelAdapter());
|
||||||
Hive.registerAdapter(FBMessageAdapter());
|
Hive.registerAdapter(FBMessageAdapter());
|
||||||
Hive.registerAdapter(KeyTokenAdapter());
|
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-logs>...');
|
print('[INIT] Load Hive<scn-logs>...');
|
||||||
|
|
||||||
@ -108,17 +106,6 @@ void main() async {
|
|||||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
|
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-keytoken-value-cache>...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Hive.deleteBoxFromDisk('scn-keytoken-value-cache');
|
|
||||||
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
|
||||||
ApplicationLog.error('Failed to open Hive-Box: scn-keytoken-value-cache' + exc.toString(), trace: trace);
|
|
||||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-keytoken-value-cache', {'error': exc.toString(), 'trace': trace});
|
|
||||||
}
|
|
||||||
|
|
||||||
print('[INIT] Load AppAuth...');
|
print('[INIT] Load AppAuth...');
|
||||||
|
|
||||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||||
@ -248,10 +235,8 @@ class SCNApp extends StatelessWidget {
|
|||||||
title: 'SimpleCloudNotifier',
|
title: 'SimpleCloudNotifier',
|
||||||
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
|
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
//TODO color settings
|
||||||
seedColor: appTheme.color.value,
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
||||||
brightness: appTheme.darkMode ? Brightness.dark : Brightness.light,
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: SCNNavLayout(),
|
home: SCNNavLayout(),
|
||||||
@ -297,7 +282,7 @@ void setFirebaseToken(String fcmToken) async {
|
|||||||
acc.setClientAndClientID(newClient);
|
acc.setClientAndClientID(newClient);
|
||||||
await acc.save();
|
await acc.save();
|
||||||
} else {
|
} else {
|
||||||
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken: fcmToken, agentModel: Globals().deviceModel, name: Globals().hostname, agentVersion: Globals().version);
|
final newClient = await APIClient.updateClient(acc, client.clientID, fcmToken, Globals().deviceModel, Globals().hostname, Globals().version);
|
||||||
acc.setClientAndClientID(newClient);
|
acc.setClientAndClientID(newClient);
|
||||||
await acc.save();
|
await acc.save();
|
||||||
}
|
}
|
||||||
@ -345,7 +330,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
|
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, null);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,13 +346,12 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
final channel = message.data['channel'] as String;
|
final channel = message.data['channel'] as String;
|
||||||
final channel_id = message.data['channel_id'] as String;
|
final channel_id = message.data['channel_id'] as String;
|
||||||
final body = message.data['body'] as String;
|
final body = message.data['body'] as String;
|
||||||
final prio = int.parse(message.data['priority'] as String);
|
|
||||||
|
|
||||||
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp, prio);
|
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.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, null);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +360,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
|||||||
} 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.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, null);
|
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,15 +71,13 @@ class Channel extends HiveObject implements FieldDebuggable {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelPreview toPreview(Subscription? sub) {
|
ChannelPreview toPreview() {
|
||||||
return ChannelPreview(
|
return ChannelPreview(
|
||||||
channelID: this.channelID,
|
channelID: this.channelID,
|
||||||
ownerUserID: this.ownerUserID,
|
ownerUserID: this.ownerUserID,
|
||||||
internalName: this.internalName,
|
internalName: this.internalName,
|
||||||
displayName: this.displayName,
|
displayName: this.displayName,
|
||||||
descriptionName: this.descriptionName,
|
descriptionName: this.descriptionName,
|
||||||
messagesSent: this.messagesSent,
|
|
||||||
subscription: sub,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,8 +109,6 @@ class ChannelPreview {
|
|||||||
final String internalName;
|
final String internalName;
|
||||||
final String displayName;
|
final String displayName;
|
||||||
final String? descriptionName;
|
final String? descriptionName;
|
||||||
final int messagesSent;
|
|
||||||
final Subscription? subscription;
|
|
||||||
|
|
||||||
const ChannelPreview({
|
const ChannelPreview({
|
||||||
required this.channelID,
|
required this.channelID,
|
||||||
@ -120,8 +116,6 @@ class ChannelPreview {
|
|||||||
required this.internalName,
|
required this.internalName,
|
||||||
required this.displayName,
|
required this.displayName,
|
||||||
required this.descriptionName,
|
required this.descriptionName,
|
||||||
required this.messagesSent,
|
|
||||||
required this.subscription,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
||||||
@ -131,8 +125,6 @@ class ChannelPreview {
|
|||||||
internalName: json['internal_name'] as String,
|
internalName: json['internal_name'] as String,
|
||||||
displayName: json['display_name'] as String,
|
displayName: json['display_name'] as String,
|
||||||
descriptionName: json['description_name'] as String?,
|
descriptionName: json['description_name'] as String?,
|
||||||
messagesSent: json['messages_sent'] as int,
|
|
||||||
subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,19 @@
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
|
|
||||||
part 'keytoken.g.dart';
|
|
||||||
|
|
||||||
@HiveType(typeId: 107)
|
|
||||||
class KeyToken {
|
class KeyToken {
|
||||||
@HiveField(0)
|
|
||||||
final String keytokenID;
|
final String keytokenID;
|
||||||
|
|
||||||
@HiveField(10)
|
|
||||||
final String name;
|
final String name;
|
||||||
@HiveField(11)
|
|
||||||
final String timestampCreated;
|
final String timestampCreated;
|
||||||
@HiveField(13)
|
final String? timestampLastused;
|
||||||
final String? timestampLastUsed;
|
|
||||||
@HiveField(14)
|
|
||||||
final String ownerUserID;
|
final String ownerUserID;
|
||||||
@HiveField(15)
|
|
||||||
final bool allChannels;
|
final bool allChannels;
|
||||||
@HiveField(16)
|
|
||||||
final List<String> channels;
|
final List<String> channels;
|
||||||
@HiveField(17)
|
|
||||||
final String permissions;
|
final String permissions;
|
||||||
@HiveField(18)
|
|
||||||
final int messagesSent;
|
final int messagesSent;
|
||||||
|
|
||||||
const KeyToken({
|
const KeyToken({
|
||||||
required this.keytokenID,
|
required this.keytokenID,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.timestampCreated,
|
required this.timestampCreated,
|
||||||
required this.timestampLastUsed,
|
required this.timestampLastused,
|
||||||
required this.ownerUserID,
|
required this.ownerUserID,
|
||||||
required this.allChannels,
|
required this.allChannels,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
@ -41,7 +26,7 @@ class KeyToken {
|
|||||||
keytokenID: json['keytoken_id'] as String,
|
keytokenID: json['keytoken_id'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
timestampCreated: json['timestamp_created'] as String,
|
timestampCreated: json['timestamp_created'] as String,
|
||||||
timestampLastUsed: json['timestamp_lastused'] as String?,
|
timestampLastused: json['timestamp_lastused'] as String?,
|
||||||
ownerUserID: json['owner_user_id'] as String,
|
ownerUserID: json['owner_user_id'] as String,
|
||||||
allChannels: json['all_channels'] as bool,
|
allChannels: json['all_channels'] as bool,
|
||||||
channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(),
|
channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(),
|
||||||
@ -53,34 +38,6 @@ class KeyToken {
|
|||||||
static List<KeyToken> fromJsonArray(List<dynamic> jsonArr) {
|
static List<KeyToken> fromJsonArray(List<dynamic> jsonArr) {
|
||||||
return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList();
|
return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyTokenPreview toPreview() {
|
|
||||||
return KeyTokenPreview(
|
|
||||||
keytokenID: keytokenID,
|
|
||||||
name: name,
|
|
||||||
ownerUserID: ownerUserID,
|
|
||||||
allChannels: allChannels,
|
|
||||||
channels: channels,
|
|
||||||
permissions: permissions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyTokenWithToken {
|
|
||||||
final KeyToken keyToken;
|
|
||||||
final String token;
|
|
||||||
|
|
||||||
KeyTokenWithToken({
|
|
||||||
required this.keyToken,
|
|
||||||
required this.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory KeyTokenWithToken.fromJson(Map<String, dynamic> json) {
|
|
||||||
return KeyTokenWithToken(
|
|
||||||
keyToken: KeyToken.fromJson(json),
|
|
||||||
token: json['token'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyTokenPreview {
|
class KeyTokenPreview {
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'keytoken.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// TypeAdapterGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
class KeyTokenAdapter extends TypeAdapter<KeyToken> {
|
|
||||||
@override
|
|
||||||
final int typeId = 107;
|
|
||||||
|
|
||||||
@override
|
|
||||||
KeyToken read(BinaryReader reader) {
|
|
||||||
final numOfFields = reader.readByte();
|
|
||||||
final fields = <int, dynamic>{
|
|
||||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
|
||||||
};
|
|
||||||
return KeyToken(
|
|
||||||
keytokenID: fields[0] as String,
|
|
||||||
name: fields[10] as String,
|
|
||||||
timestampCreated: fields[11] as String,
|
|
||||||
timestampLastUsed: fields[13] as String?,
|
|
||||||
ownerUserID: fields[14] as String,
|
|
||||||
allChannels: fields[15] as bool,
|
|
||||||
channels: (fields[16] as List).cast<String>(),
|
|
||||||
permissions: fields[17] as String,
|
|
||||||
messagesSent: fields[18] as int,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void write(BinaryWriter writer, KeyToken obj) {
|
|
||||||
writer
|
|
||||||
..writeByte(9)
|
|
||||||
..writeByte(0)
|
|
||||||
..write(obj.keytokenID)
|
|
||||||
..writeByte(10)
|
|
||||||
..write(obj.name)
|
|
||||||
..writeByte(11)
|
|
||||||
..write(obj.timestampCreated)
|
|
||||||
..writeByte(13)
|
|
||||||
..write(obj.timestampLastUsed)
|
|
||||||
..writeByte(14)
|
|
||||||
..write(obj.ownerUserID)
|
|
||||||
..writeByte(15)
|
|
||||||
..write(obj.allChannels)
|
|
||||||
..writeByte(16)
|
|
||||||
..write(obj.channels)
|
|
||||||
..writeByte(17)
|
|
||||||
..write(obj.permissions)
|
|
||||||
..writeByte(18)
|
|
||||||
..write(obj.messagesSent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => typeId.hashCode;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is KeyTokenAdapter &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
typeId == other.typeId;
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
|
||||||
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel, Error }
|
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
|
||||||
|
|
||||||
abstract class ScanResult {
|
abstract class ScanResult {
|
||||||
ScanResultMode get mode;
|
ScanResultMode get mode;
|
||||||
@ -12,10 +12,10 @@ abstract class ScanResult {
|
|||||||
final v = Uri.tryParse(lines[0]);
|
final v = Uri.tryParse(lines[0]);
|
||||||
|
|
||||||
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('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: v.queryParameters['preset_user_key'], url: lines[0]);
|
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')) {
|
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
|
||||||
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null, url: lines[0]);
|
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,12 +24,12 @@ abstract class ScanResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
|
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
|
||||||
if (lines.length != 5) return null;
|
if (lines.length != 4) return null;
|
||||||
|
|
||||||
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ScanResultError(message: 'Invalid QR code');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createChannelQR(Channel channel) {
|
static String createChannelQR(Channel channel) {
|
||||||
@ -44,9 +44,8 @@ abstract class ScanResult {
|
|||||||
class ScanResultMessageSend extends ScanResult {
|
class ScanResultMessageSend extends ScanResult {
|
||||||
final String userID;
|
final String userID;
|
||||||
final String? userKey;
|
final String? userKey;
|
||||||
final String url;
|
|
||||||
|
|
||||||
ScanResultMessageSend({required this.userID, required this.userKey, required this.url});
|
ScanResultMessageSend({required this.userID, required this.userKey});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ScanResultMode get mode => ScanResultMode.MessageSend;
|
ScanResultMode get mode => ScanResultMode.MessageSend;
|
||||||
@ -74,12 +73,3 @@ class ScanResultChannelSubscribe extends ScanResult {
|
|||||||
@override
|
@override
|
||||||
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
|
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanResultError extends ScanResult {
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
ScanResultError({required this.message});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ScanResultMode get mode => ScanResultMode.Error;
|
|
||||||
}
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
class SendMessageResponse {
|
|
||||||
final bool success;
|
|
||||||
final int errorID;
|
|
||||||
final int errorHighlight;
|
|
||||||
final String message;
|
|
||||||
final bool suppressSend;
|
|
||||||
final int messageCount;
|
|
||||||
final int quota;
|
|
||||||
final bool isPro;
|
|
||||||
final int quotaMax;
|
|
||||||
final String scnMessageID;
|
|
||||||
|
|
||||||
SendMessageResponse({
|
|
||||||
required this.success,
|
|
||||||
required this.errorID,
|
|
||||||
required this.errorHighlight,
|
|
||||||
required this.message,
|
|
||||||
required this.suppressSend,
|
|
||||||
required this.messageCount,
|
|
||||||
required this.quota,
|
|
||||||
required this.isPro,
|
|
||||||
required this.quotaMax,
|
|
||||||
required this.scnMessageID,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory SendMessageResponse.fromJson(Map<String, dynamic> json) {
|
|
||||||
return SendMessageResponse(
|
|
||||||
success: json['success'] as bool,
|
|
||||||
errorID: json['error'] as int,
|
|
||||||
errorHighlight: json['errhighlight'] as int,
|
|
||||||
message: json['message'] as String,
|
|
||||||
suppressSend: json['suppress_send'] as bool,
|
|
||||||
messageCount: json['messagecount'] as int,
|
|
||||||
quota: json['quota'] as int,
|
|
||||||
isPro: json['is_pro'] as bool,
|
|
||||||
quotaMax: json['quota_max'] as int,
|
|
||||||
scnMessageID: json['scn_msg_id'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'success': success,
|
|
||||||
'error': errorID,
|
|
||||||
'errhighlight': errorHighlight,
|
|
||||||
'message': message,
|
|
||||||
'suppress_send': suppressSend,
|
|
||||||
'messagecount': messageCount,
|
|
||||||
'quota': quota,
|
|
||||||
'is_pro': isPro,
|
|
||||||
'quota_max': quotaMax,
|
|
||||||
'scn_msg_id': scnMessageID,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ class Subscription {
|
|||||||
final String channelInternalName;
|
final String channelInternalName;
|
||||||
final String timestampCreated;
|
final String timestampCreated;
|
||||||
final bool confirmed;
|
final bool confirmed;
|
||||||
final bool active;
|
|
||||||
|
|
||||||
const Subscription({
|
const Subscription({
|
||||||
required this.subscriptionID,
|
required this.subscriptionID,
|
||||||
@ -16,7 +15,6 @@ class Subscription {
|
|||||||
required this.channelInternalName,
|
required this.channelInternalName,
|
||||||
required this.timestampCreated,
|
required this.timestampCreated,
|
||||||
required this.confirmed,
|
required this.confirmed,
|
||||||
required this.active,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Subscription.fromJson(Map<String, dynamic> json) {
|
factory Subscription.fromJson(Map<String, dynamic> json) {
|
||||||
@ -28,7 +26,6 @@ class Subscription {
|
|||||||
channelInternalName: json['channel_internal_name'] as String,
|
channelInternalName: json['channel_internal_name'] as String,
|
||||||
timestampCreated: json['timestamp_created'] as String,
|
timestampCreated: json['timestamp_created'] as String,
|
||||||
confirmed: json['confirmed'] as bool,
|
confirmed: json['confirmed'] as bool,
|
||||||
active: json['active'] as bool,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import 'package:simplecloudnotifier/pages/send/send.dart';
|
|||||||
import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart';
|
import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart';
|
||||||
import 'package:simplecloudnotifier/pages/account/account.dart';
|
import 'package:simplecloudnotifier/pages/account/account.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_list.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_list.dart';
|
||||||
import 'package:simplecloudnotifier/pages/settings/settings_view.dart';
|
import 'package:simplecloudnotifier/pages/settings/root.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
|
||||||
|
@ -5,21 +5,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.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/components/error_display/error_display.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/pages/channel_list/channel_list_extended.dart';
|
||||||
import 'package:simplecloudnotifier/pages/client_list/client_list.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/sender_list/sender_list.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/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';
|
||||||
@ -35,13 +28,13 @@ class AccountRootPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AccountRootPageState extends State<AccountRootPage> {
|
class _AccountRootPageState extends State<AccountRootPage> {
|
||||||
ImmediateFuture<int> _futureSubscriptionCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureSubscriptionCount;
|
||||||
ImmediateFuture<int> _futureClientCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureClientCount;
|
||||||
ImmediateFuture<int> _futureKeyCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureKeyCount;
|
||||||
ImmediateFuture<int> _futureChannelAllCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureChannelAllCount;
|
||||||
ImmediateFuture<int> _futureChannelOwnedCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||||
ImmediateFuture<int> _futureSenderNamesCount = ImmediateFuture.ofPending();
|
late ImmediateFuture<int>? futureSenderNamesCount;
|
||||||
ImmediateFuture<User> _futureUser = ImmediateFuture.ofPending();
|
late ImmediateFuture<User>? futureUser;
|
||||||
|
|
||||||
late AppAuth userAcc;
|
late AppAuth userAcc;
|
||||||
|
|
||||||
@ -91,51 +84,51 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _createFutures() {
|
void _createFutures() {
|
||||||
_futureSubscriptionCount = ImmediateFuture.ofPending();
|
futureSubscriptionCount = null;
|
||||||
_futureClientCount = ImmediateFuture.ofPending();
|
futureClientCount = null;
|
||||||
_futureKeyCount = ImmediateFuture.ofPending();
|
futureKeyCount = null;
|
||||||
_futureChannelAllCount = ImmediateFuture.ofPending();
|
futureChannelAllCount = null;
|
||||||
_futureChannelOwnedCount = ImmediateFuture.ofPending();
|
futureChannelSubscribedCount = null;
|
||||||
_futureSenderNamesCount = ImmediateFuture.ofPending();
|
futureSenderNamesCount = null;
|
||||||
|
|
||||||
if (userAcc.isAuth()) {
|
if (userAcc.isAuth()) {
|
||||||
_futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureChannelOwnedCount = ImmediateFuture.ofFuture(() async {
|
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
return subs.length;
|
return subs.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureClientCount = ImmediateFuture.ofFuture(() async {
|
futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
return clients.length;
|
return clients.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureKeyCount = ImmediateFuture.ofFuture(() async {
|
futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
return keys.length;
|
return keys.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
|
futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
||||||
return senders.length;
|
return senders.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
_futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +142,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
// refresh all data and then replace teh futures used in build()
|
// refresh all data and then replace teh futures used in build()
|
||||||
|
|
||||||
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||||
|
final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
@ -156,12 +150,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
final user = await userAcc.loadUser(force: true);
|
final user = await userAcc.loadUser(force: true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
||||||
_futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length);
|
||||||
_futureClientCount = ImmediateFuture.ofValue(clients.length);
|
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||||
_futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||||
_futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||||
_futureUser = ImmediateFuture.ofValue(user);
|
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
||||||
|
futureUser = ImmediateFuture.ofValue(user);
|
||||||
});
|
});
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
||||||
@ -182,12 +177,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
return _buildNoAuth(context);
|
return _buildNoAuth(context);
|
||||||
} else {
|
} else {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: _futureUser.future,
|
future: futureUser!.future,
|
||||||
builder: ((context, snapshot) {
|
builder: ((context, snapshot) {
|
||||||
if (_futureUser.value != null) {
|
if (futureUser?.value != null) {
|
||||||
return _buildShowAccount(context, acc, _futureUser.value!);
|
return _buildShowAccount(context, acc, futureUser!.value!);
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return _buildShowAccount(context, acc, snapshot.data!);
|
return _buildShowAccount(context, acc, snapshot.data!);
|
||||||
} else {
|
} else {
|
||||||
@ -344,12 +339,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: _futureChannelOwnedCount.future,
|
future: futureChannelAllCount!.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (_futureChannelOwnedCount.value != null) {
|
if (futureChannelAllCount?.value != null) {
|
||||||
return Text('${_futureChannelOwnedCount.value}');
|
return Text('${futureChannelAllCount!.value}');
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
|
||||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(color: Colors.red));
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return Text('${snapshot.data}');
|
return Text('${snapshot.data}');
|
||||||
} else {
|
} else {
|
||||||
@ -366,15 +359,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
UI.buttonIconOnly(
|
UI.buttonIconOnly(
|
||||||
onPressed: _changeUsername,
|
onPressed: () {/*TODO*/},
|
||||||
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: () {
|
onPressed: () {/*TODO*/},
|
||||||
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
|
|
||||||
},
|
|
||||||
icon: FontAwesomeIcons.cartCircleArrowUp,
|
icon: FontAwesomeIcons.cartCircleArrowUp,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -385,11 +376,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
|
|
||||||
List<Widget> _buildCards(BuildContext context, User user) {
|
List<Widget> _buildCards(BuildContext context, User user) {
|
||||||
return [
|
return [
|
||||||
_buildNumberCard(context, 'Subscription', 's', _futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
|
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Client', 's', _futureClientCount, () => Navi.push(context, () => ClientListPage())),
|
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Key', 's', _futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
|
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
||||||
_buildNumberCard(context, 'Channel', 's', _futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
|
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {
|
||||||
_buildNumberCard(context, 'Sender', '', _futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
|
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),
|
||||||
@ -400,26 +393,22 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {/*TODO*/},
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: "All Messages", filter: MessageFilter(senderUserID: [user.userID])));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int> future, void Function() action) {
|
Widget _buildNumberCard(BuildContext context, String txt, ImmediateFuture<int>? future, void Function() action) {
|
||||||
return UI.buttonCard(
|
return UI.buttonCard(
|
||||||
context: context,
|
context: context,
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: future.future,
|
future: future?.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (future.value != null) {
|
if (future?.value != null) {
|
||||||
return Text('${future.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${future?.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
|
||||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
} else {
|
} else {
|
||||||
@ -428,20 +417,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
FutureBuilder(
|
Text(txt, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
future: future.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (future.value != null) {
|
|
||||||
return Text('${txt}${((future.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
|
||||||
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
|
||||||
return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
|
||||||
} else {
|
|
||||||
return Text(txt, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: action,
|
onTap: action,
|
||||||
@ -524,50 +500,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _deleteAccount() async {
|
void _deleteAccount() async {
|
||||||
final acc = AppAuth();
|
//TODO
|
||||||
if (!acc.isAuth()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
TODO ASK BEFORE DELETING TEH FUCKING USER !!!!!!!
|
|
||||||
|
|
||||||
await APIClient.deleteUser(acc, acc.userID!);
|
|
||||||
|
|
||||||
Toaster.info('Logout', 'Successfully logged out');
|
|
||||||
|
|
||||||
acc.clear();
|
|
||||||
await acc.save();
|
|
||||||
|
|
||||||
//TODO clear messages/channels/etc in open views
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to delete user');
|
|
||||||
ApplicationLog.error('Failed to delete user: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _changeUsername() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
if (!acc.isAuth()) return;
|
|
||||||
|
|
||||||
var newusername = await UIDialogs.showTextInput(context, 'Change your public username', 'Enter new username');
|
|
||||||
if (newusername == null) return;
|
|
||||||
|
|
||||||
newusername = newusername.trim();
|
|
||||||
if (newusername == '') {
|
|
||||||
Toaster.error("Error", 'Username cannot be empty');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
|
|
||||||
setState(() {
|
|
||||||
_futureUser = ImmediateFuture.ofValue(user);
|
|
||||||
});
|
|
||||||
Toaster.success("Success", 'Username changed');
|
|
||||||
|
|
||||||
_backgroundRefresh();
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to update username');
|
|
||||||
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,9 +122,9 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
|||||||
try {
|
try {
|
||||||
setState(() => loading = true);
|
setState(() => loading = true);
|
||||||
|
|
||||||
var uid = _ctrlUserID.text;
|
final uid = _ctrlUserID.text;
|
||||||
var atokv = _ctrlTokenAdmin.text;
|
final atokv = _ctrlTokenAdmin.text;
|
||||||
var stokv = _ctrlTokenSend.text;
|
final stokv = _ctrlTokenSend.text;
|
||||||
|
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
final fcmToken = await FirebaseMessaging.instance.getToken();
|
||||||
|
|
||||||
@ -147,10 +147,6 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
|||||||
Toaster.error("Error", 'Send token does not have required permissions');
|
Toaster.error("Error", 'Send token does not have required permissions');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
final toks = await APIClient.createKeyToken(DirectTokenSource(uid, atokv), "SendKey (auto generated by SCN)", "CS", true);
|
|
||||||
|
|
||||||
stokv = toks.token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
|
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);
|
||||||
|
@ -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_scanner/channel_scanner.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';
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package: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/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/pages/channel_scanner/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';
|
||||||
@ -144,13 +142,6 @@ class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
heroTag: 'fab_channel_list_extended-plus',
|
|
||||||
onPressed: () {
|
|
||||||
Navi.push(context, () => ChannelScannerPage());
|
|
||||||
},
|
|
||||||
child: const Icon(FontAwesomeIcons.plus),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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: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';
|
||||||
@ -8,7 +9,6 @@ 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/pages/channel_view/channel_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/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';
|
||||||
@ -20,6 +20,8 @@ enum ChannelListItemMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChannelListItem extends StatefulWidget {
|
class ChannelListItem extends StatefulWidget {
|
||||||
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
const ChannelListItem({
|
const ChannelListItem({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required this.onChannelListReloadTrigger,
|
required this.onChannelListReloadTrigger,
|
||||||
@ -62,8 +64,6 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
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)),
|
||||||
@ -95,7 +95,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
(widget.channel.timestampLastSent == null) ? '' : dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
|
(widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -138,68 +138,32 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIcon(BuildContext context) {
|
Widget _buildIcon(BuildContext context) {
|
||||||
final acc = AppAuth();
|
if (widget.subscription == null) {
|
||||||
|
Widget result = Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
||||||
if (widget.subscription == null && widget.channel.ownerUserID == acc.userID) {
|
|
||||||
// not-subscribed (own channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _subscribe(), child: result);
|
result = GestureDetector(onTap: () => _subscribe(), child: result);
|
||||||
return result;
|
return result;
|
||||||
} else if (widget.subscription == null) {
|
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||||
// not-subscribed (foreign channel)
|
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||||
|
return result;
|
||||||
|
} else if (widget.subscription!.confirmed) {
|
||||||
|
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 {
|
||||||
|
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
||||||
|
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||||
return result;
|
return result;
|
||||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
|
|
||||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
|
||||||
// inactive (own channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// inactive (foreign channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.outline.withAlpha(75), size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _activate(widget.subscription!), child: result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
|
|
||||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
|
||||||
// subscribed+active (own channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// subscribed+active (foreign channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _deactivate(widget.subscription!), child: result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} else if (!widget.subscription!.confirmed) {
|
|
||||||
if (widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
|
||||||
// requested (own channel)
|
|
||||||
return SizedBox(width: 32, height: 32);
|
|
||||||
} else {
|
|
||||||
// requested (foreign channel)
|
|
||||||
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32);
|
|
||||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback
|
|
||||||
return SizedBox(width: 32, height: 32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubscriptionStateText(BuildContext context) {
|
Widget _buildSubscriptionStateText(BuildContext context) {
|
||||||
if (widget.subscription == null) {
|
if (widget.subscription == null) {
|
||||||
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||||
} else if (widget.subscription!.confirmed && widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
} 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)));
|
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
} else if (widget.subscription!.confirmed) {
|
||||||
return Text("inactive (own channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
return Text("subscripted (foreign channe)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||||
} else if (widget.subscription!.confirmed && widget.subscription!.active) {
|
|
||||||
return Text("subscribed & active (foreign channel)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
|
||||||
} else if (widget.subscription!.confirmed && !widget.subscription!.active) {
|
|
||||||
return Text("subscribed (foreign channel) (inactive)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
|
||||||
} else {
|
} else {
|
||||||
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||||
}
|
}
|
||||||
@ -230,12 +194,12 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
void _unsubscribe(Subscription sub) async {
|
void _unsubscribe(Subscription sub) async {
|
||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
if (acc.isAuth()) {
|
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID() && widget.subscription != null) {
|
||||||
try {
|
try {
|
||||||
await APIClient.deleteSubscription(acc, sub.channelID, sub.subscriptionID);
|
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
|
||||||
widget.onChannelListReloadTrigger.call();
|
widget.onChannelListReloadTrigger.call();
|
||||||
|
|
||||||
widget.onSubscriptionChanged.call(sub.channelID, null);
|
widget.onSubscriptionChanged.call(widget.channel.channelID, null);
|
||||||
|
|
||||||
Toaster.success("Success", 'Unsubscribed from channel');
|
Toaster.success("Success", 'Unsubscribed from channel');
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
@ -244,40 +208,4 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deactivate(Subscription sub) async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (acc.isAuth()) {
|
|
||||||
try {
|
|
||||||
var newSub = await APIClient.deactivateSubscription(acc, sub.channelID, sub.subscriptionID);
|
|
||||||
widget.onChannelListReloadTrigger.call();
|
|
||||||
|
|
||||||
widget.onSubscriptionChanged.call(sub.channelID, newSub);
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Unsubscribed from channel');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
|
||||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _activate(Subscription sub) async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (acc.isAuth()) {
|
|
||||||
try {
|
|
||||||
var newSub = await APIClient.activateSubscription(acc, sub.channelID, sub.subscriptionID);
|
|
||||||
widget.onChannelListReloadTrigger.call();
|
|
||||||
|
|
||||||
widget.onSubscriptionChanged.call(sub.channelID, newSub);
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Subscribed to channel');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
|
||||||
ApplicationLog.error('Failed to subscribe to 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
|||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.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/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelview.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_messagesend.dart';
|
|
||||||
import 'package:simplecloudnotifier/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),
|
|
||||||
if (scanResult == null) ...[
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(UI.DefaultBorderRadius),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 300,
|
|
||||||
width: 300,
|
|
||||||
child: MobileScanner(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
controller: _controller,
|
|
||||||
onDetect: _handleBarcode,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxWidth: 300, minWidth: 300, minHeight: 200),
|
|
||||||
child: _buildScanResult(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleBarcode(BarcodeCapture barcodes) {
|
|
||||||
setState(() {
|
|
||||||
if (barcodes.barcodes.isEmpty) {
|
|
||||||
scanResult = null;
|
|
||||||
} else {
|
|
||||||
scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? '');
|
|
||||||
print('parsed: ${jsonEncode(barcodes.barcodes[0].rawValue)} as ${scanResult.runtimeType.toString()}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScanResult(BuildContext context) {
|
|
||||||
if (scanResult == null) {
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(48)),
|
|
||||||
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult! is ScanResultMessageSend) {
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: ChannelScannerResultMessageSend(value: scanResult! as ScanResultMessageSend),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult! is ScanResultChannel) {
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: ChannelScannerResultChannelView(value: scanResult! as ScanResultChannel),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult! is ScanResultChannelSubscribe) {
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: ChannelScannerResultChannelSubscribe(value: scanResult! as ScanResultChannelSubscribe),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanResult! is ScanResultError) {
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
|
||||||
Text((scanResult! as ScanResultError).message, textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return UI.box(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
|
||||||
context: context,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
|
||||||
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,203 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
|
|
||||||
class ChannelScannerResultChannelSubscribe extends StatefulWidget {
|
|
||||||
final ScanResultChannelSubscribe value;
|
|
||||||
|
|
||||||
const ChannelScannerResultChannelSubscribe({required this.value}) : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChannelScannerResultChannelSubscribe> createState() => _ChannelScannerResultChannelSubscribeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChannelScannerResultChannelSubscribeState extends State<ChannelScannerResultChannelSubscribe> {
|
|
||||||
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
|
||||||
|
|
||||||
_ChannelScannerResultChannelSubscribeState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
|
||||||
|
|
||||||
Subscription? overrideSubscription = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
setState(() {
|
|
||||||
_fetchDataFuture = _fetchData(auth);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
|
||||||
ChannelPreview? channel = null;
|
|
||||||
try {
|
|
||||||
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPreview? user = null;
|
|
||||||
try {
|
|
||||||
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (channel, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
|
||||||
future: _fetchDataFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.data == null) {
|
|
||||||
return Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
|
||||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final (channel, user) = snapshot.data!;
|
|
||||||
|
|
||||||
final sub = overrideSubscription ?? channel.subscription;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text((user.username ?? user.userID) + ((auth.userID != null && auth.userID! == user.userID) ? "\n(you)" : "")), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(sub)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
if (sub == null)
|
|
||||||
UI.button(
|
|
||||||
text: 'Request Subscription',
|
|
||||||
onPressed: _onSubscribe,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
if (sub != null && sub.confirmed)
|
|
||||||
UI.button(
|
|
||||||
text: 'Go to channel',
|
|
||||||
onPressed: () {
|
|
||||||
Navi.pushOnRoot(context, () => ChannelViewPage(channelID: widget.value.channelID, preloadedData: null, needsReload: null));
|
|
||||||
},
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSubscribe() async {
|
|
||||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
try {
|
|
||||||
var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey);
|
|
||||||
if (sub.confirmed) {
|
|
||||||
Toaster.success("Success", "Subscription request sent and auto-confirmed");
|
|
||||||
} else {
|
|
||||||
Toaster.success("Success", "Subscription request sent - pending confirmation");
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
overrideSubscription = sub;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Toaster.error("Error", 'Failed to send subscription-request: ${e.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatSubscriptionStatus(Subscription? sub) {
|
|
||||||
if (sub == null) {
|
|
||||||
return "Not Subscribed";
|
|
||||||
} else if (sub.confirmed) {
|
|
||||||
if (sub.active) {
|
|
||||||
return "Already Subscribed";
|
|
||||||
} else {
|
|
||||||
return "Already Subscribed (inactive)";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "Unconfirmed Subscription";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
|
|
||||||
class ChannelScannerResultChannelView extends StatefulWidget {
|
|
||||||
final ScanResultChannel value;
|
|
||||||
|
|
||||||
const ChannelScannerResultChannelView({required this.value}) : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChannelScannerResultChannelView> createState() => _ChannelScannerResultChannelViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChannelScannerResultChannelViewState extends State<ChannelScannerResultChannelView> {
|
|
||||||
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
|
||||||
|
|
||||||
_ChannelScannerResultChannelViewState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
setState(() {
|
|
||||||
_fetchDataFuture = _fetchData(auth);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
|
||||||
ChannelPreview? channel = null;
|
|
||||||
try {
|
|
||||||
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPreview? user = null;
|
|
||||||
try {
|
|
||||||
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (channel, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
|
||||||
future: _fetchDataFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.data == null) {
|
|
||||||
return Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
|
||||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final (channel, user) = snapshot.data!;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(user.username ?? user.userID), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(channel.subscription)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
Text('QR Code contains no subscription-key\nCannot subscribe to channel', textAlign: TextAlign.center, style: const TextStyle(fontStyle: FontStyle.italic)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatSubscriptionStatus(Subscription? sub) {
|
|
||||||
if (sub == null) {
|
|
||||||
return "Not Subscribed";
|
|
||||||
} else if (sub.confirmed) {
|
|
||||||
if (sub.active) {
|
|
||||||
return "Already Subscribed";
|
|
||||||
} else {
|
|
||||||
return "Already Subscribed (inactive)";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "Unconfirmed Subscription";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,243 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scan_result.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class ChannelScannerResultMessageSend extends StatefulWidget {
|
|
||||||
final ScanResultMessageSend value;
|
|
||||||
|
|
||||||
const ChannelScannerResultMessageSend({required this.value}) : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChannelScannerResultMessageSend> createState() => _ChannelScannerResultMessageSendState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChannelScannerResultMessageSendState extends State<ChannelScannerResultMessageSend> {
|
|
||||||
Future<(UserPreview, KeyTokenPreview?)?> _fetchDataFuture;
|
|
||||||
|
|
||||||
_ChannelScannerResultMessageSendState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
|
||||||
|
|
||||||
late TextEditingController _ctrlMessage;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_ctrlMessage = TextEditingController();
|
|
||||||
|
|
||||||
final auth = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
setState(() {
|
|
||||||
_fetchDataFuture = _fetchData(auth);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_ctrlMessage.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<(UserPreview, KeyTokenPreview?)?> _fetchData(AppAuth auth) async {
|
|
||||||
UserPreview? user = null;
|
|
||||||
try {
|
|
||||||
user = await APIClient.getUserPreview(auth, widget.value.userID);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.userID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyTokenPreview? key = null;
|
|
||||||
if (widget.value.userKey != null) {
|
|
||||||
try {
|
|
||||||
key = await APIClient.getKeyTokenPreviewByToken(auth, widget.value.userKey!);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to fetch keytoken preview: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to fetch keytoken (preview) for ${widget.value.userID}', trace: stackTrace);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (user, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<(UserPreview, KeyTokenPreview?)?>(
|
|
||||||
future: _fetchDataFuture,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.data == null) {
|
|
||||||
return Column(
|
|
||||||
spacing: 32,
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
|
||||||
Text("Failed to parse QR", textAlign: TextAlign.center),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final (user, key) = snapshot.data!;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text((widget.value.userKey == null) ? "SCN User" : "SCN User & Key", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (user.username != null)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(user.username!), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (key != null) ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("KeyID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(key.keytokenID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("KeyName:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(child: SingleChildScrollView(child: Text(key.name), scrollDirection: Axis.horizontal)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ConstrainedBox(child: Text("Permissions:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Text(_formatPermissions(key.permissions) + "\n" + (key.allChannels ? "(all channels)" : '(${key.channels.length} channels)')),
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (widget.value.userKey == null)
|
|
||||||
Text(
|
|
||||||
'QR Code contains no key\nCannot send messages',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
),
|
|
||||||
if (widget.value.userKey != null) ..._buildSend(context),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildSend(BuildContext context) {
|
|
||||||
return [
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _ctrlMessage,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Text',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: UI.button(
|
|
||||||
text: 'Send Message',
|
|
||||||
onPressed: _onSend,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
UI.buttonIconOnly(
|
|
||||||
icon: FontAwesomeIcons.earthAmericas,
|
|
||||||
onPressed: _onOpenWeb,
|
|
||||||
square: true,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSend() async {
|
|
||||||
if (_ctrlMessage.text.isEmpty) {
|
|
||||||
Toaster.error("Error", 'Please enter a message');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.value.userKey == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text);
|
|
||||||
Toaster.success("Success", 'Message sent');
|
|
||||||
setState(() {
|
|
||||||
_ctrlMessage.clear();
|
|
||||||
});
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onOpenWeb() async {
|
|
||||||
try {
|
|
||||||
final Uri uri = Uri.parse(widget.value.url);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]');
|
|
||||||
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri);
|
|
||||||
} else {
|
|
||||||
Toaster.error("Error", 'Cannot open URL on this system');
|
|
||||||
}
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${widget.value.url}', trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatPermissions(String v) {
|
|
||||||
var splt = v.split(';');
|
|
||||||
|
|
||||||
if (splt.length == 0) return "None";
|
|
||||||
|
|
||||||
List<String> result = [];
|
|
||||||
|
|
||||||
if (splt.contains("A")) result.add(" - Admin");
|
|
||||||
if (splt.contains("UR")) result.add(" - Read Account");
|
|
||||||
if (splt.contains("CR")) result.add(" - Read Messages");
|
|
||||||
if (splt.contains("CS")) result.add(" - Send Messages");
|
|
||||||
|
|
||||||
return result.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,20 +3,16 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
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/error_display/error_display.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/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';
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.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/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';
|
||||||
@ -44,9 +40,9 @@ enum EditState { none, editing, saving }
|
|||||||
enum ChannelViewPageInitState { loading, okay, error }
|
enum ChannelViewPageInitState { loading, okay, error }
|
||||||
|
|
||||||
class _ChannelViewPageState extends State<ChannelViewPage> {
|
class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||||
ImmediateFuture<String?> _futureSubscribeKey = ImmediateFuture.ofPending();
|
late ImmediateFuture<String?> _futureSubscribeKey;
|
||||||
ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions = ImmediateFuture.ofPending();
|
late ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions;
|
||||||
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
late ImmediateFuture<UserPreview> _futureOwner;
|
||||||
|
|
||||||
final TextEditingController _ctrlDisplayName = TextEditingController();
|
final TextEditingController _ctrlDisplayName = TextEditingController();
|
||||||
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
||||||
@ -77,27 +73,20 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
if (widget.preloadedData != null && usePreload) {
|
if (widget.preloadedData != null && usePreload) {
|
||||||
|
channelPreview = widget.preloadedData!.$1.toPreview();
|
||||||
channel = widget.preloadedData!.$1;
|
channel = widget.preloadedData!.$1;
|
||||||
subscription = widget.preloadedData!.$2;
|
subscription = widget.preloadedData!.$2;
|
||||||
channelPreview = widget.preloadedData!.$1.toPreview(widget.preloadedData!.$2);
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
||||||
setState(() {
|
channelPreview = p;
|
||||||
channelPreview = p;
|
|
||||||
subscription = p.subscription;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (p.ownerUserID == userAcc.userID) {
|
if (p.ownerUserID == userAcc.userID) {
|
||||||
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
||||||
setState(() {
|
channel = r.channel;
|
||||||
channel = r.channel;
|
subscription = r.subscription;
|
||||||
subscription = r.subscription;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
channel = null;
|
||||||
channel = null;
|
subscription = null; //TODO get own subscription on this channel, even though its foreign channel
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
||||||
@ -108,34 +97,32 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
this.loadingState = ChannelViewPageInitState.okay;
|
||||||
this.loadingState = ChannelViewPageInitState.okay;
|
|
||||||
|
|
||||||
assert(channelPreview != null);
|
assert(channelPreview != null);
|
||||||
|
|
||||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||||
if (this.channel != null && this.channel!.subscribeKey != null) {
|
if (this.channel != null && this.channel!.subscribeKey != null) {
|
||||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(this.channel!.subscribeKey);
|
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(this.channel!.subscribeKey);
|
||||||
} else {
|
|
||||||
_futureSubscribeKey = ImmediateFuture<String?>.ofFuture(_getSubscribeKey(userAcc));
|
|
||||||
}
|
|
||||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofFuture(_listSubscriptions(userAcc));
|
|
||||||
} else {
|
} else {
|
||||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(null);
|
_futureSubscribeKey = ImmediateFuture<String?>.ofFuture(_getSubscribeKey(userAcc));
|
||||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofValue([]);
|
|
||||||
}
|
}
|
||||||
|
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofFuture(_listSubscriptions(userAcc));
|
||||||
|
} else {
|
||||||
|
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(null);
|
||||||
|
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofValue([]);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||||
var cacheUser = userAcc.getUserOrNull();
|
var cacheUser = userAcc.getUserOrNull();
|
||||||
if (cacheUser != null) {
|
if (cacheUser != null) {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
||||||
} else {
|
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -154,7 +141,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
if (loadingState == ChannelViewPageInitState.loading) {
|
if (loadingState == ChannelViewPageInitState.loading) {
|
||||||
child = Center(child: CircularProgressIndicator());
|
child = Center(child: CircularProgressIndicator());
|
||||||
} else if (loadingState == ChannelViewPageInitState.error) {
|
} else if (loadingState == ChannelViewPageInitState.error) {
|
||||||
child = ErrorDisplay(errorMessage: errorMessage);
|
child = Center(child: Text('Error: ' + errorMessage)); //TODO better error
|
||||||
} else if (loadingState == ChannelViewPageInitState.okay && channelPreview!.ownerUserID == userAcc.userID) {
|
} else if (loadingState == ChannelViewPageInitState.okay && channelPreview!.ownerUserID == userAcc.userID) {
|
||||||
child = _buildOwnedChannelView(context, this.channel!);
|
child = _buildOwnedChannelView(context, this.channel!);
|
||||||
} else {
|
} else {
|
||||||
@ -170,7 +157,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOwnedChannelView(BuildContext context, Channel channel) {
|
Widget _buildOwnedChannelView(BuildContext context, Channel channel) {
|
||||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
|
||||||
final isSubscribed = (subscription != null && subscription!.confirmed);
|
final isSubscribed = (subscription != null && subscription!.confirmed);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
@ -200,8 +186,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||||
title: 'Subscription (own)',
|
title: 'Subscription (own)',
|
||||||
values: [_formatSubscriptionStatus(this.subscription)],
|
values: [_formatSubscriptionStatus(this.subscription)],
|
||||||
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, null, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
|
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
),
|
),
|
||||||
_buildForeignSubscriptions(context),
|
_buildForeignSubscriptions(context),
|
||||||
_buildOwnerCard(context, true),
|
_buildOwnerCard(context, true),
|
||||||
@ -214,7 +199,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
Navi.push(context, () => ChannelMessageViewPage(channel: channel));
|
Navi.push(context, () => ChannelMessageViewPage(channel: channel));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (channel.ownerUserID == userAccUserID) UI.button(text: "Delete Channel", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -222,53 +206,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildForeignChannelView(BuildContext context, ChannelPreview channel) {
|
Widget _buildForeignChannelView(BuildContext context, ChannelPreview channel) {
|
||||||
Widget subCard;
|
final isSubscribed = (subscription != null && subscription!.confirmed);
|
||||||
|
|
||||||
if (subscription != null && subscription!.confirmed && subscription!.active) {
|
|
||||||
subCard = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
|
||||||
title: 'Subscription (foreign)',
|
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
|
||||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, null, _deactivate)],
|
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
);
|
|
||||||
} else if (subscription != null && subscription!.confirmed && !subscription!.active) {
|
|
||||||
subCard = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
|
||||||
title: 'Subscription (foreign)',
|
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
|
||||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really (permantenly) delete your subscription to this channel?')), (FontAwesomeIcons.solidSquareRss, null, _activate)],
|
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
);
|
|
||||||
} else if (subscription != null && !subscription!.confirmed) {
|
|
||||||
subCard = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
|
||||||
title: 'Subscription (foreign)',
|
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
|
||||||
iconActions: [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _unsubscribe(confirm: 'Really withdraw your subscription-request to this channel?'))],
|
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
);
|
|
||||||
} else if (subscription == null) {
|
|
||||||
subCard = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
|
||||||
title: 'Subscription (foreign)',
|
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
|
||||||
iconActions: [(FontAwesomeIcons.solidSquareRss, null, _subscribe)],
|
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
subCard = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidDiagramSubtask,
|
|
||||||
title: 'Subscription (foreign)',
|
|
||||||
values: [_formatSubscriptionStatus(subscription)],
|
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -291,16 +229,15 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
),
|
),
|
||||||
_buildDisplayNameCard(context, false),
|
_buildDisplayNameCard(context, false),
|
||||||
_buildDescriptionNameCard(context, false),
|
_buildDescriptionNameCard(context, false),
|
||||||
subCard,
|
|
||||||
_buildForeignSubscriptions(context),
|
|
||||||
_buildOwnerCard(context, false),
|
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidEnvelope,
|
icon: FontAwesomeIcons.solidDiagramSubtask,
|
||||||
title: 'Messages',
|
title: 'Subscription (foreign)',
|
||||||
values: [channel.messagesSent.toString()],
|
values: [_formatSubscriptionStatus(subscription)],
|
||||||
mainAction: (subscription != null && subscription!.confirmed) ? () => Navi.push(context, () => FilteredMessageViewPage(title: channel.displayName, filter: MessageFilter(channelIDs: [channel.channelID]))) : null,
|
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
|
||||||
),
|
),
|
||||||
|
_buildForeignSubscriptions(context),
|
||||||
|
_buildOwnerCard(context, false),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -321,8 +258,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
icon: FontAwesomeIcons.solidDiagramSuccessor,
|
icon: FontAwesomeIcons.solidDiagramSuccessor,
|
||||||
title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')',
|
title: 'Subscription (' + (user?.username ?? user?.userID ?? 'other') + ')',
|
||||||
values: [_formatSubscriptionStatus(sub)],
|
values: [_formatSubscriptionStatus(sub)],
|
||||||
iconActions: _getForeignIncomingSubActions(sub),
|
iconActions: _getForeignSubActions(sub),
|
||||||
mainAction: () => Navi.push(context, () => SubscriptionViewPage(subscriptionID: subscription!.subscriptionID, preloadedData: (subscription, null, null, null), needsReload: widget.needsReload)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -424,7 +360,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
icon: FontAwesomeIcons.solidInputText,
|
icon: FontAwesomeIcons.solidInputText,
|
||||||
title: 'DisplayName',
|
title: 'DisplayName',
|
||||||
values: [_displayNameOverride ?? channelPreview!.displayName],
|
values: [_displayNameOverride ?? channelPreview!.displayName],
|
||||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDisplayName)] : [],
|
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDisplayName)] : [],
|
||||||
);
|
);
|
||||||
} else if (_editDisplayName == EditState.saving) {
|
} else if (_editDisplayName == EditState.saving) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -480,7 +416,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
icon: FontAwesomeIcons.solidInputPipe,
|
icon: FontAwesomeIcons.solidInputPipe,
|
||||||
title: 'Description',
|
title: 'Description',
|
||||||
values: [_descriptionNameOverride ?? channelPreview?.descriptionName ?? ''],
|
values: [_descriptionNameOverride ?? channelPreview?.descriptionName ?? ''],
|
||||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditDescriptionName)] : [],
|
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditDescriptionName)] : [],
|
||||||
);
|
);
|
||||||
} else if (_editDescriptionName == EditState.saving) {
|
} else if (_editDescriptionName == EditState.saving) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -589,16 +525,11 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unsubscribe({String? confirm = null}) async {
|
void _unsubscribe() async {
|
||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
if (subscription == null) return;
|
if (subscription == null) return;
|
||||||
|
|
||||||
if (confirm != null) {
|
|
||||||
final r = await UIDialogs.showConfirmDialog(context, confirm, okText: 'Unsubscribe', cancelText: 'Cancel');
|
|
||||||
if (!r) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||||
widget.needsReload?.call();
|
widget.needsReload?.call();
|
||||||
@ -612,47 +543,11 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deactivate() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.deactivateSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
|
||||||
widget.needsReload?.call();
|
|
||||||
|
|
||||||
await _initStateAsync(false);
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Unsubscribed from channel');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
|
||||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _activate() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.activateSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
|
||||||
widget.needsReload?.call();
|
|
||||||
|
|
||||||
await _initStateAsync(false);
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Subscribed to channel');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
|
||||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cancelForeignSubscription(Subscription sub) async {
|
void _cancelForeignSubscription(Subscription sub) async {
|
||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await APIClient.unconfirmSubscription(acc, widget.channelID, sub.subscriptionID);
|
await APIClient.unconfirmSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||||
widget.needsReload?.call();
|
widget.needsReload?.call();
|
||||||
|
|
||||||
await _initStateAsync(false);
|
await _initStateAsync(false);
|
||||||
@ -668,7 +563,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await APIClient.confirmSubscription(acc, widget.channelID, sub.subscriptionID);
|
await APIClient.confirmSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||||
widget.needsReload?.call();
|
widget.needsReload?.call();
|
||||||
|
|
||||||
await _initStateAsync(false);
|
await _initStateAsync(false);
|
||||||
@ -684,7 +579,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await APIClient.deleteSubscription(acc, widget.channelID, sub.subscriptionID);
|
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
|
||||||
widget.needsReload?.call();
|
widget.needsReload?.call();
|
||||||
|
|
||||||
await _initStateAsync(false);
|
await _initStateAsync(false);
|
||||||
@ -699,14 +594,10 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
String _formatSubscriptionStatus(Subscription? subscription) {
|
String _formatSubscriptionStatus(Subscription? subscription) {
|
||||||
if (subscription == null) {
|
if (subscription == null) {
|
||||||
return 'Not Subscribed';
|
return 'Not Subscribed';
|
||||||
} else if (subscription.confirmed && subscription.active) {
|
} else if (subscription.confirmed) {
|
||||||
return 'Subscribed & Active';
|
return 'Subscribed';
|
||||||
} else if (subscription.confirmed && !subscription.active) {
|
|
||||||
return 'Subscribed & Inactive';
|
|
||||||
} else if (!subscription.confirmed) {
|
|
||||||
return 'Requested';
|
|
||||||
} else {
|
} else {
|
||||||
return '?';
|
return 'Requested';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -760,13 +651,13 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<(IconData, Color?, void Function())> _getForeignIncomingSubActions(Subscription sub) {
|
List<(IconData, void Function())> _getForeignSubActions(Subscription sub) {
|
||||||
if (sub.confirmed) {
|
if (sub.confirmed) {
|
||||||
return [(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _cancelForeignSubscription(sub))];
|
return [(FontAwesomeIcons.solidSquareXmark, () => _cancelForeignSubscription(sub))];
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
(FontAwesomeIcons.solidSquareCheck, Colors.green[900], () => _confirmForeignSubscription(sub)),
|
(FontAwesomeIcons.solidSquareCheck, () => _confirmForeignSubscription(sub)),
|
||||||
(FontAwesomeIcons.solidSquareXmark, Colors.red[900], () => _denyForeignSubscription(sub)),
|
(FontAwesomeIcons.solidSquareXmark, () => _denyForeignSubscription(sub)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
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/client.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/client_list/client_list_item.dart';
|
|
||||||
|
|
||||||
class ClientListPage extends StatefulWidget {
|
|
||||||
const ClientListPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ClientListPage> createState() => _ClientListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ClientListPageState extends State<ClientListPage> {
|
|
||||||
final PagingController<int, Client> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(_fetchPage);
|
|
||||||
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
ApplicationLog.debug('ClientListPage::dispose');
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Start ClientListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
_pagingController.error = 'Not logged in';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final items = (await APIClient.getClientList(acc)).toList();
|
|
||||||
|
|
||||||
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
|
|
||||||
|
|
||||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
_pagingController.error = exc.toString();
|
|
||||||
ApplicationLog.error('Failed to list clients: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SCNScaffold(
|
|
||||||
title: "Clients",
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(
|
|
||||||
() => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
child: PagedListView<int, Client>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<Client>(
|
|
||||||
itemBuilder: (context, item, index) => ClientListItem(item: item),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/client.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
|
|
||||||
enum ClientListItemMode {
|
|
||||||
Messages,
|
|
||||||
Extended,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ClientListItem extends StatelessWidget {
|
|
||||||
const ClientListItem({
|
|
||||||
required this.item,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Client item;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return Card.filled(
|
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildIcon(context),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
item.name ?? item.clientID,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
dateFormat.format(DateTime.parse(item.timestampCreated).toLocal()),
|
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(item.agentModel.toString() + " " + item.agentVersion.toString()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIcon(BuildContext context) {
|
|
||||||
if (item.type == "ANDROID") return Icon(FontAwesomeIcons.android, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
if (item.type == "IOS") return Icon(FontAwesomeIcons.apple, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
if (item.type == "LINUX") return Icon(FontAwesomeIcons.linux, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
if (item.type == "MACOS") return Icon(FontAwesomeIcons.appleWhole, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
if (item.type == "WINDOWS") return Icon(FontAwesomeIcons.windows, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
|
|
||||||
return Icon(FontAwesomeIcons.solidSignature, color: Theme.of(context).colorScheme.outline, size: 32);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,4 @@
|
|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/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';
|
||||||
@ -54,12 +49,6 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
|||||||
text: 'Show Simple Notification',
|
text: 'Show Simple Notification',
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
UI.button(
|
|
||||||
big: false,
|
|
||||||
onPressed: _copyToken,
|
|
||||||
text: 'Query+Copy FCM Token',
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
UI.button(
|
UI.button(
|
||||||
big: false,
|
big: false,
|
||||||
onPressed: _sendTokenToServer,
|
onPressed: _sendTokenToServer,
|
||||||
@ -68,32 +57,8 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
|||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
UI.button(
|
UI.button(
|
||||||
big: false,
|
big: false,
|
||||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null),
|
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null),
|
||||||
text: 'Show local notification (generic)',
|
text: 'Show local notification',
|
||||||
),
|
|
||||||
UI.button(
|
|
||||||
big: false,
|
|
||||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 0),
|
|
||||||
text: 'Show local notification (Prio = 0)',
|
|
||||||
),
|
|
||||||
UI.button(
|
|
||||||
big: false,
|
|
||||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 1),
|
|
||||||
text: 'Show local notification (Prio = 1)',
|
|
||||||
),
|
|
||||||
UI.button(
|
|
||||||
big: false,
|
|
||||||
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 2),
|
|
||||||
text: 'Show local notification (Prio = 2)',
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
UI.button(
|
|
||||||
big: false,
|
|
||||||
onPressed: () {
|
|
||||||
AppSettings().update((p) => p.reset());
|
|
||||||
Toaster.success("Success", "AppSettings reset to default");
|
|
||||||
},
|
|
||||||
text: 'Reset AppSettings to default',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -102,46 +67,7 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendTokenToServer() async {
|
void _sendTokenToServer() {
|
||||||
try {
|
//TODO
|
||||||
final auth = AppAuth();
|
|
||||||
|
|
||||||
final clientID = auth.getClientID();
|
|
||||||
if (clientID == null) {
|
|
||||||
Toaster.error("Error", "No Client set");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
|
||||||
if (fcmToken == null) {
|
|
||||||
Toaster.error("Error", "No FCM token returned from Firebase");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newClient = await APIClient.updateClient(auth, clientID, fcmToken: fcmToken);
|
|
||||||
auth.setClientAndClientID(newClient);
|
|
||||||
|
|
||||||
Toaster.success("Success", "Token sent to server");
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", "An error occurred while sending the token: ${exc.toString()}");
|
|
||||||
ApplicationLog.error("An error occurred while sending the token: ${exc.toString()}", trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copyToken() async {
|
|
||||||
try {
|
|
||||||
final fcmToken = await FirebaseMessaging.instance.getToken();
|
|
||||||
if (fcmToken == null) {
|
|
||||||
Toaster.error("Error", "No FCM token returned from Firebase");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Clipboard.setData(new ClipboardData(text: fcmToken));
|
|
||||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
|
||||||
print('================= [CLIPBOARD] =================\n${fcmToken}\n================= [/CLIPBOARD] =================');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", "An error occurred while sending the token: ${exc.toString()}");
|
|
||||||
ApplicationLog.error("An error occurred while sending the token: ${exc.toString()}", trace: trace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ class DebugLogsPage extends StatefulWidget {
|
|||||||
class _DebugLogsPageState extends State<DebugLogsPage> {
|
class _DebugLogsPageState extends State<DebugLogsPage> {
|
||||||
Box<SCNLog> logBox = Hive.box<SCNLog>('scn-logs');
|
Box<SCNLog> logBox = Hive.box<SCNLog>('scn-logs');
|
||||||
|
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -23,7 +23,7 @@ class _DebugMainPageState extends State<DebugMainPage> {
|
|||||||
DebugMainPageSubPage.actions: DebugActionsPage(),
|
DebugMainPageSubPage.actions: DebugActionsPage(),
|
||||||
};
|
};
|
||||||
|
|
||||||
DebugMainPageSubPage _subPage = DebugMainPageSubPage.logs;
|
DebugMainPageSubPage _subPage = DebugMainPageSubPage.colors;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -53,11 +53,11 @@ class _DebugMainPageState extends State<DebugMainPage> {
|
|||||||
return SegmentedButton<DebugMainPageSubPage>(
|
return SegmentedButton<DebugMainPageSubPage>(
|
||||||
showSelectedIcon: false,
|
showSelectedIcon: false,
|
||||||
segments: const <ButtonSegment<DebugMainPageSubPage>>[
|
segments: const <ButtonSegment<DebugMainPageSubPage>>[
|
||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.colors, icon: Icon(FontAwesomeIcons.solidPaintRoller, size: 14)),
|
||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.actions, icon: Icon(FontAwesomeIcons.solidHammer, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.actions, icon: Icon(FontAwesomeIcons.solidHammer, size: 14)),
|
||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.requests, icon: Icon(FontAwesomeIcons.solidNetworkWired, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.requests, icon: Icon(FontAwesomeIcons.solidNetworkWired, size: 14)),
|
||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.persistence, icon: Icon(FontAwesomeIcons.solidDatabase, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.persistence, icon: Icon(FontAwesomeIcons.solidDatabase, size: 14)),
|
||||||
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.colors, icon: Icon(FontAwesomeIcons.solidPaintRoller, size: 14)),
|
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
|
||||||
],
|
],
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
|
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class _DebugFailureLogFilePageState extends State<DebugFailureLogFilePage> {
|
|||||||
if (_futureContent?.value != null) {
|
if (_futureContent?.value != null) {
|
||||||
return _buildContent(context, _futureContent!.value!);
|
return _buildContent(context, _futureContent!.value!);
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return _buildContent(context, snapshot.data!);
|
return _buildContent(context, snapshot.data!);
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,9 +6,7 @@ class DebugSharedPrefPage extends StatelessWidget {
|
|||||||
final SharedPreferences sharedPref;
|
final SharedPreferences sharedPref;
|
||||||
final List<String> keys;
|
final List<String> keys;
|
||||||
|
|
||||||
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList() {
|
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList();
|
||||||
keys.sort((a, b) => a.compareTo(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
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';
|
||||||
@ -8,19 +6,11 @@ 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 StatefulWidget {
|
class DebugRequestViewPage extends StatelessWidget {
|
||||||
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(
|
||||||
@ -33,23 +23,22 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
...buildRow(context, "name", "Name", widget.request.name),
|
...buildRow(context, "Name", request.name),
|
||||||
...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()),
|
...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()),
|
||||||
...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()),
|
...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()),
|
||||||
...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()),
|
...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()),
|
||||||
Divider(),
|
Divider(),
|
||||||
...buildRow(context, "method", "Method", widget.request.method),
|
...buildRow(context, "Method", request.method),
|
||||||
...buildRow(context, "url", "URL", widget.request.url, mono: true),
|
...buildRow(context, "URL", request.url),
|
||||||
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.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
||||||
if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true),
|
if (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody),
|
||||||
UI.button(text: "Copy request as curl", onPressed: _copyCurl, tonal: true),
|
|
||||||
Divider(),
|
Divider(),
|
||||||
if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()),
|
if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()),
|
||||||
if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true),
|
if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody),
|
||||||
if (widget.request.responseHeaders.isNotEmpty) ...buildRow(context, "response_headers", "Reponse->Headers", widget.request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true, json: false),
|
if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
|
||||||
Divider(),
|
Divider(),
|
||||||
if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true),
|
if (request.error != '') ...buildRow(context, "Error", request.error),
|
||||||
if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true),
|
if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -57,19 +46,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> buildRow(BuildContext context, String key, String title, String value, {bool? json, bool? mono}) {
|
List<Widget> buildRow(BuildContext context, String title, String value) {
|
||||||
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),
|
||||||
@ -87,62 +64,21 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
|||||||
},
|
},
|
||||||
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: widget.request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
|
color: 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: (isMono || isJson)
|
child: SelectableText(
|
||||||
? SingleChildScrollView(
|
value,
|
||||||
scrollDirection: Axis.horizontal,
|
minLines: 1,
|
||||||
child: SelectableText(
|
maxLines: 10,
|
||||||
value,
|
),
|
||||||
minLines: 1,
|
|
||||||
maxLines: 10,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SelectableText(
|
|
||||||
value,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 10,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _copyCurl() {
|
|
||||||
final method = '-X ${widget.request.method}';
|
|
||||||
final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' ');
|
|
||||||
final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : '';
|
|
||||||
|
|
||||||
final curlParts = ['curl', method, header, '"${widget.request.url}"', body];
|
|
||||||
|
|
||||||
final txt = curlParts.where((part) => part.isNotEmpty).join(' ');
|
|
||||||
|
|
||||||
Clipboard.setData(new ClipboardData(text: txt));
|
|
||||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
|
||||||
print('================= [CLIPBOARD] =================\n${txt}\n================= [/CLIPBOARD] =================');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class DebugRequestsPage extends StatefulWidget {
|
|||||||
class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||||
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests');
|
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests');
|
||||||
|
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -47,6 +47,10 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
@ -57,14 +61,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Text(req.type),
|
||||||
children: [
|
|
||||||
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
|
||||||
Expanded(child: SizedBox()),
|
|
||||||
Text(req.type),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Text(
|
Text(
|
||||||
req.error,
|
req.error,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -84,6 +81,10 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
@ -91,13 +92,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
|
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
subtitle: Text(req.type),
|
||||||
children: [
|
|
||||||
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
|
||||||
Expanded(child: SizedBox()),
|
|
||||||
Text(req.type),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class FilteredMessageViewPage extends StatefulWidget {
|
|
||||||
const FilteredMessageViewPage({
|
|
||||||
required this.title,
|
|
||||||
required this.filter,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final MessageFilter filter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
|
|
||||||
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
|
||||||
|
|
||||||
Map<String, Channel>? _channels = null;
|
|
||||||
bool _channelsFetched = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_channels = SCNDataCache().getChannelMap();
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(_fetchPage);
|
|
||||||
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPage(String thisPageToken) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
final cfg = Provider.of<AppSettings>(context, listen: false);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Start FilteredMessageViewPage::_pagingController::_fetchPage [ ${thisPageToken} ]');
|
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
_pagingController.error = 'Not logged in';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_channels == null || !_channelsFetched) {
|
|
||||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
|
||||||
setState(() {
|
|
||||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
|
||||||
_channelsFetched = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter, includeNonSuscribed: true);
|
|
||||||
|
|
||||||
SCNDataCache().addToMessageCache(newItems); // no await
|
|
||||||
|
|
||||||
if (npt == '@end') {
|
|
||||||
_pagingController.appendLastPage(newItems);
|
|
||||||
} else {
|
|
||||||
_pagingController.appendPage(newItems, npt);
|
|
||||||
}
|
|
||||||
} catch (exc, trace) {
|
|
||||||
_pagingController.error = exc.toString();
|
|
||||||
ApplicationLog.error('Failed to list channel-messages: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SCNScaffold(
|
|
||||||
title: this.widget.title,
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: _buildMessageList(context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMessageList(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(
|
|
||||||
() => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
child: PagedListView<String, SCNMessage>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
|
|
||||||
itemBuilder: (context, item, index) => MessageListItem(
|
|
||||||
message: item,
|
|
||||||
allChannels: _channels ?? {},
|
|
||||||
onPressed: () {
|
|
||||||
Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,216 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
|
|
||||||
class KeyTokenCreateDialog extends StatefulWidget {
|
|
||||||
final void Function(KeyToken, String) onCreated;
|
|
||||||
|
|
||||||
const KeyTokenCreateDialog({
|
|
||||||
required this.onCreated,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_KeyTokenCreateDialogState createState() => _KeyTokenCreateDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _KeyTokenCreateDialogState extends State<KeyTokenCreateDialog> {
|
|
||||||
TextEditingController _ctrlName = TextEditingController();
|
|
||||||
Set<String> selectedPermissions = {'CS'};
|
|
||||||
|
|
||||||
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
|
||||||
|
|
||||||
bool allChannels = true;
|
|
||||||
Set<String> selectedChannels = new Set<String>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_ctrlName.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Create new key'),
|
|
||||||
content: Container(
|
|
||||||
width: 0,
|
|
||||||
height: 400,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildNameCtrl(context),
|
|
||||||
SizedBox(height: 32),
|
|
||||||
_buildPermissionCtrl(context),
|
|
||||||
SizedBox(height: 32),
|
|
||||||
_buildChannelCtrl(context),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
|
||||||
child: const Text('Create'),
|
|
||||||
onPressed: _create,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNameCtrl(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: _ctrlName,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Key name',
|
|
||||||
hintText: 'Enter a name for the new key',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPermissionCtrl(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text('Permissions:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
primary: false,
|
|
||||||
itemBuilder: (builder, index) {
|
|
||||||
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
|
||||||
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text(txt),
|
|
||||||
leading: Icon(
|
|
||||||
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selectedPermissions.contains(prm)) {
|
|
||||||
selectedPermissions.remove(prm);
|
|
||||||
} else {
|
|
||||||
selectedPermissions.add(prm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: 4,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildChannelCtrl(BuildContext context) {
|
|
||||||
return FutureBuilder<List<Channel>>(
|
|
||||||
future: _futureOwnedChannels.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final ownChannels = snapshot.data!;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text('Channels:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text('All Channels'),
|
|
||||||
leading: Icon(
|
|
||||||
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
allChannels = !allChannels;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
if (!allChannels)
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
primary: false,
|
|
||||||
itemBuilder: (builder, index) {
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text(ownChannels[index].displayName),
|
|
||||||
leading: Icon(
|
|
||||||
selectedChannels.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selectedChannels.contains(ownChannels[index].channelID)) {
|
|
||||||
selectedChannels.remove(ownChannels[index].channelID);
|
|
||||||
} else {
|
|
||||||
selectedChannels.add(ownChannels[index].channelID);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: ownChannels.length,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _create() async {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
if (!userAcc.isAuth()) return;
|
|
||||||
|
|
||||||
if (_ctrlName.text.isEmpty) {
|
|
||||||
Toaster.error('Missing data', 'Please enter a name for the key');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final perm = selectedPermissions.join(';');
|
|
||||||
final channels = allChannels ? <String>[] : selectedChannels.toList();
|
|
||||||
|
|
||||||
var kt = await APIClient.createKeyToken(userAcc, _ctrlName.text, perm, allChannels, channels: channels);
|
|
||||||
Toaster.success('Success', 'Key created successfully');
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
widget.onCreated(kt.keyToken, kt.token);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to create keytoken: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to create key: ${exc.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
|
|
||||||
class KeyTokenCreatedModal extends StatelessWidget {
|
|
||||||
final KeyToken keytoken;
|
|
||||||
final String tokenValue;
|
|
||||||
|
|
||||||
const KeyTokenCreatedModal({
|
|
||||||
Key? key,
|
|
||||||
required this.keytoken,
|
|
||||||
required this.tokenValue,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('A new key was created'),
|
|
||||||
content: Container(
|
|
||||||
width: 0,
|
|
||||||
height: 350,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'KeyTokenID',
|
|
||||||
values: [keytoken.keytokenID],
|
|
||||||
),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidInputText,
|
|
||||||
title: 'Name',
|
|
||||||
values: [keytoken.name],
|
|
||||||
),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
|
||||||
title: 'Permissions',
|
|
||||||
values: _formatPermissions(keytoken.permissions),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
const BadgeDisplay(
|
|
||||||
text: "Please copy and save the token now, it cannot be retrieved later.",
|
|
||||||
icon: null,
|
|
||||||
mode: BadgeMode.warn,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidKey,
|
|
||||||
title: 'Token',
|
|
||||||
values: [tokenValue.substring(0, 12) + '...'],
|
|
||||||
iconActions: [(FontAwesomeIcons.copy, null, _copy)],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: const Text('Close'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _formatPermissions(String v) {
|
|
||||||
var splt = v.split(';');
|
|
||||||
|
|
||||||
if (splt.length == 0) return ["None"];
|
|
||||||
|
|
||||||
List<String> result = [];
|
|
||||||
|
|
||||||
if (splt.contains("A")) result.add("Admin");
|
|
||||||
if (splt.contains("UR")) result.add("Read Account");
|
|
||||||
if (splt.contains("CR")) result.add("Read Messages");
|
|
||||||
if (splt.contains("CS")) result.add("Send Messages");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _copy() {
|
|
||||||
Clipboard.setData(new ClipboardData(text: tokenValue));
|
|
||||||
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
|
||||||
print('================= [CLIPBOARD] =================\n${tokenValue}\n================= [/CLIPBOARD] =================');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
|
|
||||||
|
|
||||||
class KeyTokenListPage extends StatefulWidget {
|
|
||||||
const KeyTokenListPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<KeyTokenListPage> createState() => _KeyTokenListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _KeyTokenListPageState extends State<KeyTokenListPage> {
|
|
||||||
final PagingController<int, KeyToken> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(_fetchPage);
|
|
||||||
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
ApplicationLog.debug('KeyTokenListPage::dispose');
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Start KeyTokenListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
_pagingController.error = 'Not logged in';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final items = (await APIClient.getKeyTokenList(acc)).toList();
|
|
||||||
|
|
||||||
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
|
|
||||||
|
|
||||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
_pagingController.error = exc.toString();
|
|
||||||
ApplicationLog.error('Failed to list keytokens: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SCNScaffold(
|
|
||||||
title: "Keys",
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(
|
|
||||||
() => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
child: PagedListView<int, KeyToken>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<KeyToken>(
|
|
||||||
itemBuilder: (context, item, index) => KeyTokenListItem(item: item, needsReload: _fullRefresh),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
heroTag: 'fab_keytokenlist_plus',
|
|
||||||
onPressed: () {
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => KeyTokenCreateDialog(onCreated: _created),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Icon(FontAwesomeIcons.plus),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _created(KeyToken token, String tokValue) {
|
|
||||||
setState(() {
|
|
||||||
_pagingController.itemList?.insert(0, token);
|
|
||||||
});
|
|
||||||
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => KeyTokenCreatedModal(keytoken: token, tokenValue: tokValue),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _fullRefresh() {
|
|
||||||
ApplicationLog.debug('KeytokenListPage::fullRefresh');
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
enum KeyTokenListItemMode {
|
|
||||||
Messages,
|
|
||||||
Extended,
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyTokenListItem extends StatelessWidget {
|
|
||||||
const KeyTokenListItem({
|
|
||||||
required this.item,
|
|
||||||
required this.needsReload,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final KeyToken item;
|
|
||||||
final void Function()? needsReload;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return Card.filled(
|
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Navi.push(context, () => KeyTokenViewPage(keytokenID: item.keytokenID, preloadedData: item, needsReload: needsReload));
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidGearCode, color: Theme.of(context).colorScheme.outline, size: 32),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
item.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
(item.timestampLastUsed == null) ? '' : dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()),
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
"Permissions: " + _formatPermissions(item.permissions, item.allChannels, item.channels),
|
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(item.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(usedKeys: [item.keytokenID])));
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatPermissions(String v, bool allChannels, List<String> channels) {
|
|
||||||
var splt = v.split(';');
|
|
||||||
|
|
||||||
if (splt.length == 0) return "None";
|
|
||||||
|
|
||||||
var a = splt.contains("A");
|
|
||||||
var ur = splt.contains("UR");
|
|
||||||
var cr = splt.contains("CR");
|
|
||||||
var cs = splt.contains("CS");
|
|
||||||
|
|
||||||
if (a) return "Admin";
|
|
||||||
if (cr && cs && allChannels) return "Read+Send";
|
|
||||||
if (cr && cs && !allChannels) return "Read+Send (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
|
||||||
if (ur && !cr && !cs) return "Account-Read";
|
|
||||||
if (cr && !cs && !allChannels) return "Read-only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
|
||||||
if (cr && !cs && allChannels) return "Read-only";
|
|
||||||
if (cs && !allChannels) return "Send-Only (${channels.length} channel${channels.length == 1 ? '' : 's'})";
|
|
||||||
if (cs && allChannels) return "Send-Only";
|
|
||||||
|
|
||||||
return "{ " + v + " | " + (allChannels ? 'all' : '${channels.length}') + " }";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
|
|
||||||
class EditKeyTokenChannelsDialog extends StatefulWidget {
|
|
||||||
final List<Channel> ownedChannels;
|
|
||||||
final KeyTokenPreview keytoken;
|
|
||||||
|
|
||||||
final void Function(Set<String>) onUpdateChannels;
|
|
||||||
final void Function() onUpdateSetAllChannels;
|
|
||||||
|
|
||||||
const EditKeyTokenChannelsDialog({
|
|
||||||
required this.ownedChannels,
|
|
||||||
required this.keytoken,
|
|
||||||
required this.onUpdateChannels,
|
|
||||||
required this.onUpdateSetAllChannels,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_EditKeyTokenChannelsDialogState createState() => _EditKeyTokenChannelsDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EditKeyTokenChannelsDialogState extends State<EditKeyTokenChannelsDialog> {
|
|
||||||
late bool allChannels;
|
|
||||||
late Set<String> selectedEntries;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
allChannels = widget.keytoken.allChannels;
|
|
||||||
selectedEntries = (widget.keytoken.channels).toSet();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var ownChannels = widget.ownedChannels.toList();
|
|
||||||
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Channels'),
|
|
||||||
content: Container(
|
|
||||||
width: 0,
|
|
||||||
height: 400,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text('All Channels'),
|
|
||||||
leading: Icon(
|
|
||||||
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
allChannels = !allChannels;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
if (!allChannels)
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemBuilder: (builder, index) {
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text(ownChannels[index].displayName),
|
|
||||||
leading: Icon(
|
|
||||||
selectedEntries.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selectedEntries.contains(ownChannels[index].channelID)) {
|
|
||||||
selectedEntries.remove(ownChannels[index].channelID);
|
|
||||||
} else {
|
|
||||||
selectedEntries.add(ownChannels[index].channelID);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: ownChannels.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
|
||||||
child: const Text('Update'),
|
|
||||||
onPressed: () {
|
|
||||||
if (allChannels) {
|
|
||||||
widget.onUpdateSetAllChannels();
|
|
||||||
} else {
|
|
||||||
widget.onUpdateChannels(selectedEntries);
|
|
||||||
}
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
|
|
||||||
class EditKeyTokenPermissionsDialog extends StatefulWidget {
|
|
||||||
final KeyTokenPreview keytoken;
|
|
||||||
|
|
||||||
final void Function(String) onUpdatePermissions;
|
|
||||||
|
|
||||||
const EditKeyTokenPermissionsDialog({
|
|
||||||
required this.keytoken,
|
|
||||||
required this.onUpdatePermissions,
|
|
||||||
Key? key,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_EditKeyTokenPermissionsDialogState createState() => _EditKeyTokenPermissionsDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EditKeyTokenPermissionsDialogState extends State<EditKeyTokenPermissionsDialog> {
|
|
||||||
Set<String> selectedPermissions = new Set<String>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
for (var p in widget.keytoken.permissions.split(';')) {
|
|
||||||
if (p.isNotEmpty) selectedPermissions.add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Permissions'),
|
|
||||||
content: Container(
|
|
||||||
width: 0,
|
|
||||||
height: 400,
|
|
||||||
child: ListView.builder(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemBuilder: (builder, index) {
|
|
||||||
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
|
||||||
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
|
||||||
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
|
||||||
title: Text(txt),
|
|
||||||
leading: Icon(
|
|
||||||
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
if (selectedPermissions.contains(prm)) {
|
|
||||||
selectedPermissions.remove(prm);
|
|
||||||
} else {
|
|
||||||
selectedPermissions.add(prm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: 4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
|
||||||
child: const Text('Update'),
|
|
||||||
onPressed: () {
|
|
||||||
widget.onUpdatePermissions(selectedPermissions.join(';'));
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,612 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_channel_modal.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_permission_modal.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class KeyTokenViewPage extends StatefulWidget {
|
|
||||||
const KeyTokenViewPage({
|
|
||||||
required this.keytokenID,
|
|
||||||
required this.preloadedData,
|
|
||||||
required this.needsReload,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String keytokenID;
|
|
||||||
final KeyToken? preloadedData;
|
|
||||||
|
|
||||||
final void Function()? needsReload;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<KeyTokenViewPage> createState() => _KeyTokenViewPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
enum EditState { none, editing, saving }
|
|
||||||
|
|
||||||
enum KeyTokenViewPageInitState { loading, okay, error }
|
|
||||||
|
|
||||||
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|
||||||
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
|
||||||
|
|
||||||
ImmediateFuture<Map<String, ChannelPreview>> _futureAllChannels = ImmediateFuture.ofPending();
|
|
||||||
|
|
||||||
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
|
||||||
|
|
||||||
final TextEditingController _ctrlName = TextEditingController();
|
|
||||||
|
|
||||||
int _loadingIndeterminateCounter = 0;
|
|
||||||
|
|
||||||
EditState _editName = EditState.none;
|
|
||||||
String? _nameOverride = null;
|
|
||||||
|
|
||||||
KeyTokenPreview? keytokenPreview;
|
|
||||||
KeyToken? keytoken;
|
|
||||||
|
|
||||||
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
|
|
||||||
String errorMessage = '';
|
|
||||||
|
|
||||||
KeyToken? keytokenUserAccAdmin;
|
|
||||||
KeyToken? keytokenUserAccSend;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_initStateAsync(true);
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initStateAsync(bool usePreload) async {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
if (widget.preloadedData != null && usePreload) {
|
|
||||||
keytoken = widget.preloadedData!;
|
|
||||||
keytokenPreview = widget.preloadedData!.toPreview();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
var p = await APIClient.getKeyTokenPreviewByID(userAcc, widget.keytokenID);
|
|
||||||
setState(() {
|
|
||||||
keytokenPreview = p;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (p.ownerUserID == userAcc.userID) {
|
|
||||||
var r = await APIClient.getKeyToken(userAcc, widget.keytokenID);
|
|
||||||
setState(() {
|
|
||||||
keytoken = r;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
keytoken = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to load data');
|
|
||||||
this.errorMessage = 'Failed to load data: ' + exc.toString();
|
|
||||||
this.loadingState = KeyTokenViewPageInitState.error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
this.loadingState = KeyTokenViewPageInitState.okay;
|
|
||||||
|
|
||||||
assert(keytokenPreview != null);
|
|
||||||
|
|
||||||
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
|
|
||||||
var cacheUser = userAcc.getUserOrNull();
|
|
||||||
if (cacheUser != null) {
|
|
||||||
_futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview());
|
|
||||||
} else {
|
|
||||||
_futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_futureAllChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.allAny).then((lst) async {
|
|
||||||
Map<String, ChannelPreview> result = {};
|
|
||||||
|
|
||||||
for (var c in lst) result[c.channel.channelID] = c.channel.toPreview(c.subscription);
|
|
||||||
|
|
||||||
if (keytokenPreview != null) {
|
|
||||||
for (var cid in keytokenPreview!.channels) {
|
|
||||||
if (!result.containsKey(cid)) {
|
|
||||||
result[cid] = await APIClient.getChannelPreview(userAcc, cid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
|
||||||
});
|
|
||||||
|
|
||||||
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenAdmin!).then((token) {
|
|
||||||
setState(() {
|
|
||||||
keytokenUserAccAdmin = token;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenSend!).then((token) {
|
|
||||||
setState(() {
|
|
||||||
keytokenUserAccSend = token;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_ctrlName.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
var title = "Key";
|
|
||||||
|
|
||||||
Widget child;
|
|
||||||
|
|
||||||
if (loadingState == KeyTokenViewPageInitState.loading) {
|
|
||||||
child = Center(child: CircularProgressIndicator());
|
|
||||||
} else if (loadingState == KeyTokenViewPageInitState.error) {
|
|
||||||
child = ErrorDisplay(errorMessage: errorMessage);
|
|
||||||
} else if (loadingState == KeyTokenViewPageInitState.okay && keytokenPreview!.ownerUserID == userAcc.userID) {
|
|
||||||
child = _buildOwnedKeyTokenView(context, this.keytoken!);
|
|
||||||
title = this.keytoken!.name;
|
|
||||||
} else {
|
|
||||||
child = _buildForeignKeyTokenView(context, this.keytokenPreview!);
|
|
||||||
title = keytokenPreview!.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SCNScaffold(
|
|
||||||
title: title,
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 8),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'KeyTokenID',
|
|
||||||
values: [
|
|
||||||
keytoken.keytokenID,
|
|
||||||
if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)',
|
|
||||||
if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)',
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_buildNameCard(context, true),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidClock,
|
|
||||||
title: 'Created',
|
|
||||||
values: [dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
|
|
||||||
),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidClockTwo,
|
|
||||||
title: 'Last Used',
|
|
||||||
values: [(keytoken.timestampLastUsed == null) ? 'Never' : dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
|
|
||||||
),
|
|
||||||
_buildOwnerCard(context, true),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidEnvelope,
|
|
||||||
title: 'Messages',
|
|
||||||
values: [keytoken.messagesSent.toString()],
|
|
||||||
mainAction: () {
|
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, filter: MessageFilter(usedKeys: [keytoken.keytokenID])));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
..._buildPermissionCard(context, true, keytoken.toPreview()),
|
|
||||||
UI.button(text: "Delete Key", onPressed: _deleteKey, color: Colors.red[900]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForeignKeyTokenView(BuildContext context, KeyTokenPreview keytoken) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 8),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'KeyTokenID',
|
|
||||||
values: [keytoken.keytokenID],
|
|
||||||
),
|
|
||||||
_buildNameCard(context, false),
|
|
||||||
_buildOwnerCard(context, false),
|
|
||||||
..._buildPermissionCard(context, false, keytoken),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOwnerCard(BuildContext context, bool isOwned) {
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _futureOwner.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Owner',
|
|
||||||
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Owner',
|
|
||||||
values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : '')],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNameCard(BuildContext context, bool isOwned) {
|
|
||||||
if (_editName == EditState.editing) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
|
||||||
child: UI.box(
|
|
||||||
context: context,
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: _ctrlName,
|
|
||||||
decoration: new InputDecoration.collapsed(hintText: 'Name'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
IconButton(icon: FaIcon(FontAwesomeIcons.solidFloppyDisk), onPressed: _saveName),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (_editName == EditState.none) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidInputText,
|
|
||||||
title: 'Name',
|
|
||||||
values: [_nameOverride ?? keytokenPreview!.name],
|
|
||||||
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, null, _showEditName)] : [],
|
|
||||||
);
|
|
||||||
} else if (_editName == EditState.saving) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
|
|
||||||
child: UI.box(
|
|
||||||
context: context,
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(child: SizedBox()),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Padding(padding: const EdgeInsets.all(8.0), child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator())),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw 'Invalid EditDisplayNameState: $_editName';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showEditName() {
|
|
||||||
setState(() {
|
|
||||||
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
|
|
||||||
_editName = EditState.editing;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveName() async {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
final newName = _ctrlName.text;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setState(() {
|
|
||||||
_editName = EditState.saving;
|
|
||||||
});
|
|
||||||
|
|
||||||
final newKeyToken = await APIClient.updateKeyToken(userAcc, widget.keytokenID, name: newName);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_editName = EditState.none;
|
|
||||||
_nameOverride = newKeyToken.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
widget.needsReload?.call();
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to save DisplayName');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<UserPreview> _getOwner(AppAuth auth) async {
|
|
||||||
try {
|
|
||||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
|
||||||
|
|
||||||
_incLoadingIndeterminateCounter(1);
|
|
||||||
|
|
||||||
final owner = APIClient.getUserPreview(auth, keytokenPreview!.ownerUserID);
|
|
||||||
|
|
||||||
//await Future.delayed(const Duration(seconds: 10), () {});
|
|
||||||
|
|
||||||
return owner;
|
|
||||||
} finally {
|
|
||||||
_incLoadingIndeterminateCounter(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _incLoadingIndeterminateCounter(int delta) {
|
|
||||||
setState(() {
|
|
||||||
_loadingIndeterminateCounter += delta;
|
|
||||||
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildPermissionCard(BuildContext context, bool isOwned, KeyTokenPreview keyToken) {
|
|
||||||
Widget w1;
|
|
||||||
Widget w2;
|
|
||||||
|
|
||||||
if (isOwned) {
|
|
||||||
w1 = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
|
||||||
title: 'Permissions',
|
|
||||||
values: _formatPermissions(keyToken.permissions),
|
|
||||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
w1 = UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidShieldKeyhole,
|
|
||||||
title: 'Permissions',
|
|
||||||
values: _formatPermissions(keyToken.permissions),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
w2 = FutureBuilder(
|
|
||||||
future: _futureAllChannels.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
var cmap = snapshot.data!;
|
|
||||||
if (isOwned) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channels',
|
|
||||||
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
|
||||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channels',
|
|
||||||
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isOwned) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channels',
|
|
||||||
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
|
||||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channels',
|
|
||||||
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return [w1, w2];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _formatPermissions(String v) {
|
|
||||||
var splt = v.split(';');
|
|
||||||
|
|
||||||
if (splt.length == 0) return ["None"];
|
|
||||||
|
|
||||||
List<String> result = [];
|
|
||||||
|
|
||||||
if (splt.contains("A")) result.add("Admin");
|
|
||||||
if (splt.contains("UR")) result.add("Read Account");
|
|
||||||
if (splt.contains("CR")) result.add("Read Messages");
|
|
||||||
if (splt.contains("CS")) result.add("Send Messages");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editPermissions() async {
|
|
||||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot edit the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot edit the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => EditKeyTokenPermissionsDialog(
|
|
||||||
keytoken: keytokenPreview!,
|
|
||||||
onUpdatePermissions: _updatePermissions,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editChannels() async {
|
|
||||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot edit the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot edit the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownChannels = (await _futureOwnedChannels.future);
|
|
||||||
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
|
||||||
|
|
||||||
await showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => EditKeyTokenChannelsDialog(
|
|
||||||
ownedChannels: ownChannels,
|
|
||||||
keytoken: keytokenPreview!,
|
|
||||||
onUpdateChannels: _updateChannelsSelected,
|
|
||||||
onUpdateSetAllChannels: _updateChannelsAll,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteKey() async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot delete the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
|
||||||
Toaster.error("Error", "You cannot delete the currently used token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final r = await UIDialogs.showConfirmDialog(context, 'Really (permanently) delete this Key?', okText: 'Delete', cancelText: 'Cancel');
|
|
||||||
if (!r) return;
|
|
||||||
|
|
||||||
await APIClient.deleteKeyToken(acc, keytokenPreview!.keytokenID);
|
|
||||||
widget.needsReload?.call();
|
|
||||||
|
|
||||||
Toaster.info('Logout', 'Successfully deleted the key');
|
|
||||||
|
|
||||||
Navi.pop(context);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to delete key');
|
|
||||||
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateChannelsSelected(Set<String> selectedEntries) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: selectedEntries.toList(), allChannels: false);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
keytoken = r;
|
|
||||||
keytokenPreview = r.toPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
Toaster.info("Success", "Key updated");
|
|
||||||
|
|
||||||
widget.needsReload?.call();
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to update key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateChannelsAll() async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: [], allChannels: true);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
keytoken = r;
|
|
||||||
keytokenPreview = r.toPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
Toaster.info("Success", "Key updated");
|
|
||||||
|
|
||||||
widget.needsReload?.call();
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to update key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updatePermissions(String perm) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, permissions: perm);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
keytoken = r;
|
|
||||||
keytokenPreview = r.toPreview();
|
|
||||||
});
|
|
||||||
|
|
||||||
Toaster.info("Success", "Key updated");
|
|
||||||
|
|
||||||
widget.needsReload?.call();
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to update key');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
|
|
||||||
enum MessageFilterChipletType {
|
enum MessageFilterChipletType {
|
||||||
search,
|
search,
|
||||||
plainSearch,
|
|
||||||
channel,
|
channel,
|
||||||
sender,
|
sender,
|
||||||
timeRange,
|
timeRange,
|
||||||
@ -22,8 +21,6 @@ class MessageFilterChiplet {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case MessageFilterChipletType.search:
|
case MessageFilterChipletType.search:
|
||||||
return FontAwesomeIcons.magnifyingGlass;
|
return FontAwesomeIcons.magnifyingGlass;
|
||||||
case MessageFilterChipletType.plainSearch:
|
|
||||||
return FontAwesomeIcons.magnifyingGlassPlus;
|
|
||||||
case MessageFilterChipletType.channel:
|
case MessageFilterChipletType.channel:
|
||||||
return FontAwesomeIcons.snake;
|
return FontAwesomeIcons.snake;
|
||||||
case MessageFilterChipletType.sender:
|
case MessageFilterChipletType.sender:
|
||||||
|
@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
|||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_events.dart';
|
import 'package:simplecloudnotifier/state/app_events.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
@ -30,7 +30,6 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
||||||
|
|
||||||
Map<String, Channel>? _channels = null;
|
Map<String, Channel>? _channels = null;
|
||||||
bool _channelsFetched = false;
|
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@ -136,12 +135,9 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (_channels == null || !_channelsFetched) {
|
if (_channels == null) {
|
||||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||||
setState(() {
|
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
|
||||||
_channelsFetched = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
SCNDataCache().setChannelCache(channels); // no await
|
SCNDataCache().setChannelCache(channels); // no await
|
||||||
}
|
}
|
||||||
@ -318,11 +314,6 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList();
|
filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var chipletsPlainSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.plainSearch).toList();
|
|
||||||
if (chipletsPlainSearch.isNotEmpty) {
|
|
||||||
filter.plainSearchFilter = chipletsPlainSearch.map((p) => p.value as String).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList();
|
var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList();
|
||||||
if (chipletsKeyTokens.isNotEmpty) {
|
if (chipletsKeyTokens.isNotEmpty) {
|
||||||
filter.usedKeys = chipletsKeyTokens.map((p) => p.value as String).toList();
|
filter.usedKeys = chipletsKeyTokens.map((p) => p.value as String).toList();
|
||||||
@ -338,13 +329,6 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
filter.senderNames = chipletSender.map((p) => p.value as String).toList();
|
filter.senderNames = chipletSender.map((p) => p.value as String).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
var chipletsTimeRange = _filterChiplets.where((p) => p.type == MessageFilterChipletType.timeRange).toList();
|
|
||||||
if (chipletsTimeRange.isNotEmpty) {
|
|
||||||
//TODO
|
|
||||||
//filter.timeAfter = chipletsTimeRange[0].value1 as DateTime;
|
|
||||||
//filter.timeBefore = chipletsTimeRange[0].value2 as DateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,15 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
class MessageListItem extends StatelessWidget {
|
class MessageListItem extends StatelessWidget {
|
||||||
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
static final _lineCount = 3; //TODO setting
|
||||||
|
|
||||||
const MessageListItem({
|
const MessageListItem({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.allChannels,
|
required this.allChannels,
|
||||||
@ -31,9 +32,6 @@ class MessageListItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Card buildWithoutChannel(BuildContext context) {
|
Card buildWithoutChannel(BuildContext context) {
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
||||||
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
|
|
||||||
|
|
||||||
return Card.filled(
|
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)),
|
||||||
@ -59,7 +57,7 @@ class MessageListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||||
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -72,10 +70,10 @@ class MessageListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
processContent(message.content, previewLineCount),
|
processContent(message.content),
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: previewLineCount,
|
maxLines: _lineCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.priority == 2) SizedBox(width: 4),
|
if (message.priority == 2) SizedBox(width: 4),
|
||||||
@ -92,9 +90,6 @@ class MessageListItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Card buildWithChannel(BuildContext context) {
|
Card buildWithChannel(BuildContext context) {
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
||||||
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
|
|
||||||
|
|
||||||
return Card.filled(
|
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)),
|
||||||
@ -118,7 +113,7 @@ class MessageListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Expanded(child: SizedBox()),
|
Expanded(child: SizedBox()),
|
||||||
Text(
|
Text(
|
||||||
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
|
||||||
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -137,10 +132,10 @@ class MessageListItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
processContent(message.content, previewLineCount),
|
processContent(message.content),
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: previewLineCount,
|
maxLines: _lineCount,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.priority == 2) SizedBox(width: 4),
|
if (message.priority == 2) SizedBox(width: 4),
|
||||||
@ -156,7 +151,7 @@ class MessageListItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String processContent(String? v, int lineCount) {
|
String processContent(String? v) {
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -166,7 +161,7 @@ class MessageListItem extends StatelessWidget {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.sublist(0, min(lineCount, lines.length)).join("\n").trim();
|
return lines.sublist(0, min(_lineCount, lines.length)).join("\n").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
String processTitle(String? v) {
|
String processTitle(String? v) {
|
||||||
|
@ -5,18 +5,14 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
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/error_display/error_display.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/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
import 'package:simplecloudnotifier/utils/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';
|
||||||
@ -38,8 +34,7 @@ class MessageViewPage extends StatefulWidget {
|
|||||||
class _MessageViewPageState extends State<MessageViewPage> {
|
class _MessageViewPageState extends State<MessageViewPage> {
|
||||||
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
||||||
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
||||||
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
final ScrollController _controller = ScrollController();
|
|
||||||
|
|
||||||
bool _monospaceMode = false;
|
bool _monospaceMode = false;
|
||||||
|
|
||||||
@ -66,7 +61,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
final msg = await APIClient.getMessage(acc, widget.messageID);
|
final msg = await APIClient.getMessage(acc, widget.messageID);
|
||||||
|
|
||||||
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
||||||
final fut_key = APIClient.getKeyTokenPreviewByID(acc, msg.usedKeyID);
|
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
|
||||||
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
||||||
|
|
||||||
final chn = await fut_chn;
|
final chn = await fut_chn;
|
||||||
@ -87,7 +82,6 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +99,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
final (msg, chn, tok, usr) = snapshot.data!;
|
final (msg, chn, tok, usr) = snapshot.data!;
|
||||||
return _buildMessageView(context, msg, chn, tok, usr);
|
return _buildMessageView(context, msg, chn, tok, usr);
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||||
} else if (message != null && !this.message!.trimmed) {
|
} else if (message != null && !this.message!.trimmed) {
|
||||||
return _buildMessageView(context, this.message!, null, null, null);
|
return _buildMessageView(context, this.message!, null, null, null);
|
||||||
} else {
|
} else {
|
||||||
@ -142,104 +136,73 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
||||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||||
|
|
||||||
final child = Padding(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
..._buildMessageHeader(context, message, channel),
|
children: [
|
||||||
SizedBox(height: 8),
|
..._buildMessageHeader(context, message, channel),
|
||||||
if (message.content != null) ..._buildMessageContent(context, message),
|
SizedBox(height: 8),
|
||||||
SizedBox(height: 8),
|
if (message.content != null) ..._buildMessageContent(context, message),
|
||||||
if (message.senderName != null)
|
SizedBox(height: 8),
|
||||||
|
if (message.senderName != null)
|
||||||
|
UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidSignature,
|
||||||
|
title: 'Sender',
|
||||||
|
values: [message.senderName!],
|
||||||
|
mainAction: () => {/*TODO*/},
|
||||||
|
),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidSignature,
|
icon: FontAwesomeIcons.solidGearCode,
|
||||||
title: 'Sender',
|
title: 'KeyToken',
|
||||||
values: [message.senderName!],
|
values: [message.usedKeyID, token?.name ?? '...'],
|
||||||
mainAction: () => {
|
mainAction: () => {/*TODO*/},
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!])))
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidGearCode,
|
icon: FontAwesomeIcons.solidIdCardClip,
|
||||||
title: 'KeyToken',
|
title: 'MessageID',
|
||||||
values: [message.usedKeyID, token?.name ?? '...'],
|
values: [message.messageID, message.userMessageID ?? ''],
|
||||||
mainAction: () {
|
),
|
||||||
if (message.senderUserID == userAccUserID) {
|
UI.metaCard(
|
||||||
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
|
context: context,
|
||||||
} else {
|
icon: FontAwesomeIcons.solidSnake,
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])));
|
title: 'Channel',
|
||||||
}
|
values: [message.channelID, channel?.displayName ?? message.channelInternalName],
|
||||||
},
|
mainAction: (channel != null)
|
||||||
),
|
? () {
|
||||||
UI.metaCard(
|
Navi.push(context, () => ChannelViewPage(channelID: channel.channelID, preloadedData: null, needsReload: null));
|
||||||
context: context,
|
}
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
: null,
|
||||||
title: 'MessageID',
|
),
|
||||||
values: [message.messageID, message.userMessageID ?? ''],
|
UI.metaCard(
|
||||||
),
|
context: context,
|
||||||
UI.metaCard(
|
icon: FontAwesomeIcons.solidTimer,
|
||||||
context: context,
|
title: 'Timestamp',
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
values: [message.timestamp],
|
||||||
title: 'Channel',
|
),
|
||||||
values: [message.channelID, channel?.displayName ?? message.channelInternalName],
|
UI.metaCard(
|
||||||
mainAction: (channel != null)
|
context: context,
|
||||||
? () {
|
icon: FontAwesomeIcons.solidUser,
|
||||||
Navi.push(context, () => ChannelViewPage(channelID: channel.channelID, preloadedData: null, needsReload: null));
|
title: 'User',
|
||||||
}
|
values: [user?.userID ?? '...', user?.username ?? ''],
|
||||||
: null,
|
mainAction: () => {/*TODO*/},
|
||||||
),
|
),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidTimer,
|
icon: FontAwesomeIcons.solidBolt,
|
||||||
title: 'Timestamp',
|
title: 'Priority',
|
||||||
values: [message.timestamp],
|
values: [_prettyPrintPriority(message.priority)],
|
||||||
),
|
mainAction: () => {/*TODO*/},
|
||||||
UI.metaCard(
|
),
|
||||||
context: context,
|
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
||||||
icon: FontAwesomeIcons.solidUser,
|
],
|
||||||
title: 'User',
|
),
|
||||||
values: [user?.userID ?? message.senderUserID, user?.username ?? ''],
|
|
||||||
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, filter: MessageFilter(senderUserID: [message.senderUserID]))),
|
|
||||||
),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidBolt,
|
|
||||||
title: 'Priority',
|
|
||||||
values: [_prettyPrintPriority(message.priority)],
|
|
||||||
mainAction: () => Navi.push(context, () => FilteredMessageViewPage(title: "Priority ${message.priority}", filter: MessageFilter(priority: [message.priority]))),
|
|
||||||
),
|
|
||||||
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var showScrollbar = false;
|
|
||||||
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
|
||||||
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
|
||||||
|
|
||||||
if (showScrollbar) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(0, 0, 6, 0),
|
|
||||||
child: Scrollbar(
|
|
||||||
thickness: 12.0,
|
|
||||||
radius: Radius.circular(6),
|
|
||||||
thumbVisibility: false,
|
|
||||||
interactive: true,
|
|
||||||
controller: _controller,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: _controller,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
|
String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
|
||||||
@ -247,8 +210,6 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
|
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -259,7 +220,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
Expanded(child: SizedBox()),
|
Expanded(child: SizedBox()),
|
||||||
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
|
||||||
class SendRootPage extends StatefulWidget {
|
class SendRootPage extends StatefulWidget {
|
||||||
const SendRootPage({super.key, required this.isVisiblePage});
|
const SendRootPage({super.key, required bool isVisiblePage});
|
||||||
|
|
||||||
final bool isVisiblePage;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SendRootPage> createState() => _SendRootPageState();
|
State<SendRootPage> createState() => _SendRootPageState();
|
||||||
@ -23,28 +15,18 @@ class SendRootPage extends StatefulWidget {
|
|||||||
class _SendRootPageState extends State<SendRootPage> {
|
class _SendRootPageState extends State<SendRootPage> {
|
||||||
late TextEditingController _msgTitle;
|
late TextEditingController _msgTitle;
|
||||||
late TextEditingController _msgContent;
|
late TextEditingController _msgContent;
|
||||||
late TextEditingController _channelName;
|
|
||||||
late TextEditingController _senderName;
|
|
||||||
|
|
||||||
int _priority = 0;
|
|
||||||
|
|
||||||
bool _expanded = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_msgTitle = TextEditingController();
|
_msgTitle = TextEditingController();
|
||||||
_msgContent = TextEditingController();
|
_msgContent = TextEditingController();
|
||||||
_channelName = TextEditingController();
|
|
||||||
_senderName = TextEditingController();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_msgTitle.dispose();
|
_msgTitle.dispose();
|
||||||
_msgContent.dispose();
|
_msgContent.dispose();
|
||||||
_channelName.dispose();
|
|
||||||
_senderName.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,162 +37,52 @@ class _SendRootPageState extends State<SendRootPage> {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: _expanded ? _buildExpanded(context, acc) : _buildSimple(context, acc),
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildQRCode(context, acc),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: 1.0,
|
||||||
|
child: TextField(
|
||||||
|
controller: _msgTitle,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
labelText: 'Title',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: 1.0,
|
||||||
|
child: TextField(
|
||||||
|
controller: _msgContent,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
labelText: 'Text',
|
||||||
|
),
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: null,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
|
||||||
|
onPressed: _send,
|
||||||
|
child: const Text('Send'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSimple(BuildContext context, AppAuth acc) {
|
void _send() {
|
||||||
return Column(
|
//...
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildQRCode(context, acc),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _msgTitle,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Title',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _msgContent,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Text',
|
|
||||||
),
|
|
||||||
minLines: 2,
|
|
||||||
maxLines: null,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: UI.button(
|
|
||||||
text: 'Send',
|
|
||||||
onPressed: () {
|
|
||||||
_sendSimple(acc);
|
|
||||||
},
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
UI.buttonIconOnly(
|
|
||||||
icon: FontAwesomeIcons.layerPlus,
|
|
||||||
onPressed: _openExpanded,
|
|
||||||
square: true,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildExpanded(BuildContext context, AppAuth acc) {
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _channelName,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Channel',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _msgTitle,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Title',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _senderName,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Sender',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SegmentedButton<int>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
segments: const <ButtonSegment<int>>[
|
|
||||||
ButtonSegment<int>(value: 0, label: Text('Low Priority')),
|
|
||||||
ButtonSegment<int>(value: 1, label: Text('Normal')),
|
|
||||||
ButtonSegment<int>(value: 2, label: Text('High Priority')),
|
|
||||||
],
|
|
||||||
selected: {_priority},
|
|
||||||
onSelectionChanged: (Set<int> newSelection) {
|
|
||||||
setState(() {
|
|
||||||
_priority = newSelection.isEmpty ? 1 : newSelection.first;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1.0,
|
|
||||||
child: TextField(
|
|
||||||
controller: _msgContent,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
labelText: 'Text',
|
|
||||||
),
|
|
||||||
minLines: 6,
|
|
||||||
maxLines: null,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: UI.button(
|
|
||||||
text: 'Send',
|
|
||||||
onPressed: () {
|
|
||||||
_sendExpanded(acc);
|
|
||||||
},
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
UI.buttonIconOnly(
|
|
||||||
icon: FontAwesomeIcons.squareDashed,
|
|
||||||
onPressed: _closeExpanded,
|
|
||||||
square: true,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
iconColor: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQRCode(BuildContext context, AppAuth acc) {
|
Widget _buildQRCode(BuildContext context, AppAuth acc) {
|
||||||
@ -221,82 +93,39 @@ class _SendRootPageState extends State<SendRootPage> {
|
|||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: acc.loadUser(force: false),
|
future: acc.loadUser(force: false),
|
||||||
builder: ((context, snapshot) {
|
builder: ((context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.active || snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return const SizedBox(
|
if (snapshot.hasError) {
|
||||||
width: 300.0,
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
height: 300.0,
|
}
|
||||||
child: Center(child: CircularProgressIndicator()),
|
var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}';
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_openWeb(url);
|
||||||
|
},
|
||||||
|
child: QrImageView(
|
||||||
|
data: url,
|
||||||
|
version: QrVersions.auto,
|
||||||
|
size: 300.0,
|
||||||
|
eyeStyle: QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.square,
|
||||||
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
|
),
|
||||||
|
dataModuleStyle: QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.square,
|
||||||
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (snapshot.hasError) {
|
return const SizedBox(
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
width: 300.0,
|
||||||
}
|
height: 300.0,
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
child: Center(child: CircularProgressIndicator()),
|
||||||
return Text('...'); //?
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}';
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
_openWeb(url);
|
|
||||||
},
|
|
||||||
child: QrImageView(
|
|
||||||
data: url,
|
|
||||||
version: QrVersions.auto,
|
|
||||||
size: 300.0,
|
|
||||||
eyeStyle: QrEyeStyle(
|
|
||||||
eyeShape: QrEyeShape.square,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
),
|
|
||||||
dataModuleStyle: QrDataModuleStyle(
|
|
||||||
dataModuleShape: QrDataModuleShape.square,
|
|
||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _sendSimple(AppAuth acc) async {
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
Toaster.error("Error", 'Must be logged in to send messages');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text);
|
|
||||||
Toaster.success("Success", 'Message sent');
|
|
||||||
setState(() {
|
|
||||||
_msgTitle.clear();
|
|
||||||
_msgContent.clear();
|
|
||||||
});
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendExpanded(AppAuth acc) async {
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
Toaster.error("Error", 'Must be logged in to send messages');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text, channel: _channelName.text, senderName: _senderName.text, priority: _priority);
|
|
||||||
Toaster.success("Success", 'Message sent');
|
|
||||||
setState(() {
|
|
||||||
_msgTitle.clear();
|
|
||||||
_msgContent.clear();
|
|
||||||
});
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
|
||||||
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openWeb(String url) async {
|
void _openWeb(String url) async {
|
||||||
try {
|
try {
|
||||||
final Uri uri = Uri.parse(url);
|
final Uri uri = Uri.parse(url);
|
||||||
@ -306,30 +135,10 @@ class _SendRootPageState extends State<SendRootPage> {
|
|||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri);
|
await launchUrl(uri);
|
||||||
} else {
|
} else {
|
||||||
Toaster.error("Error", 'Cannot open URL on this system');
|
// TODO ("Cannot open URL");
|
||||||
}
|
}
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace);
|
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeExpanded() {
|
|
||||||
setState(() {
|
|
||||||
_expanded = false;
|
|
||||||
_channelName.clear();
|
|
||||||
_priority = 1;
|
|
||||||
_senderName.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openExpanded() {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_expanded = true;
|
|
||||||
_channelName.text = userAcc.getUserOrNull()?.defaultChannel ?? 'main';
|
|
||||||
_priority = 1;
|
|
||||||
_senderName.text = Globals().deviceName;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
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/sender_name_statistics.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/sender_list/sender_list_item.dart';
|
|
||||||
|
|
||||||
class SenderListPage extends StatefulWidget {
|
|
||||||
const SenderListPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SenderListPage> createState() => _SenderListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SenderListPageState extends State<SenderListPage> {
|
|
||||||
final PagingController<int, SenderNameStatistics> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(_fetchPage);
|
|
||||||
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
ApplicationLog.debug('SenderListPage::dispose');
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Start SenderListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
_pagingController.error = 'Not logged in';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final items = (await APIClient.getSenderNameList(acc)).toList();
|
|
||||||
|
|
||||||
items.sort((a, b) => -1 * a.lastTimestamp.compareTo(b.lastTimestamp));
|
|
||||||
|
|
||||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
_pagingController.error = exc.toString();
|
|
||||||
ApplicationLog.error('Failed to list senders: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SCNScaffold(
|
|
||||||
title: "Sender",
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(
|
|
||||||
() => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
child: PagedListView<int, SenderNameStatistics>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<SenderNameStatistics>(
|
|
||||||
itemBuilder: (context, item, index) => SenderListItem(item: item),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
enum SenderListItemMode {
|
|
||||||
Messages,
|
|
||||||
Extended,
|
|
||||||
}
|
|
||||||
|
|
||||||
class SenderListItem extends StatelessWidget {
|
|
||||||
const SenderListItem({
|
|
||||||
required this.item,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final SenderNameStatistics item;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return Card.filled(
|
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidSignature, color: Theme.of(context).colorScheme.outline, size: 32),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
item.name,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
dateFormat.format(DateTime.parse(item.lastTimestamp).toLocal()),
|
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(item.count.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
17
flutter/lib/pages/settings/root.dart
Normal file
17
flutter/lib/pages/settings/root.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SettingsRootPage extends StatefulWidget {
|
||||||
|
const SettingsRootPage({super.key, required bool isVisiblePage});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsRootPageState extends State<SettingsRootPage> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Text('Settings'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,117 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
class SettingsNumberModal extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final int currentValue;
|
|
||||||
final int minValue;
|
|
||||||
final int maxValue;
|
|
||||||
final ValueChanged<int> onValueChanged;
|
|
||||||
|
|
||||||
const SettingsNumberModal({
|
|
||||||
Key? key,
|
|
||||||
required this.title,
|
|
||||||
required this.currentValue,
|
|
||||||
required this.minValue,
|
|
||||||
required this.maxValue,
|
|
||||||
required this.onValueChanged,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SettingsNumberModal> createState() => _SettingsNumberModalState();
|
|
||||||
|
|
||||||
static Future<void> show(
|
|
||||||
BuildContext context, {
|
|
||||||
required String title,
|
|
||||||
required int currentValue,
|
|
||||||
required int minValue,
|
|
||||||
required int maxValue,
|
|
||||||
required ValueChanged<int> onValueChanged,
|
|
||||||
}) {
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => SettingsNumberModal(
|
|
||||||
title: title,
|
|
||||||
currentValue: currentValue,
|
|
||||||
minValue: minValue,
|
|
||||||
maxValue: maxValue,
|
|
||||||
onValueChanged: onValueChanged,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingsNumberModalState extends State<SettingsNumberModal> {
|
|
||||||
late TextEditingController _controller;
|
|
||||||
late int selectedValue;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
selectedValue = widget.currentValue;
|
|
||||||
_controller = TextEditingController(text: widget.currentValue.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(widget.title),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: _controller,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: 'Enter a number',
|
|
||||||
errorText: _validateInput(),
|
|
||||||
),
|
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
selectedValue = int.tryParse(value) ?? widget.currentValue;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: _validateInput() == null
|
|
||||||
? () {
|
|
||||||
widget.onValueChanged(selectedValue);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: const Text('OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validateInput() {
|
|
||||||
final number = int.tryParse(_controller.text);
|
|
||||||
if (number == null) {
|
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
if (number < widget.minValue) {
|
|
||||||
return 'Value must be at least ${widget.minValue}';
|
|
||||||
}
|
|
||||||
if (number > widget.maxValue) {
|
|
||||||
return 'Value must be at most ${widget.maxValue}';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
|
||||||
|
|
||||||
class SettingsPickerScreen<T> extends StatelessWidget {
|
|
||||||
const SettingsPickerScreen({
|
|
||||||
Key? key,
|
|
||||||
required this.title,
|
|
||||||
required this.initialValue,
|
|
||||||
required this.values,
|
|
||||||
required this.onValueChanged,
|
|
||||||
this.icons,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final T initialValue;
|
|
||||||
final List<T> values;
|
|
||||||
final void Function(T value) onValueChanged;
|
|
||||||
final Widget Function(T v)? icons;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text(title)),
|
|
||||||
body: SettingsList(
|
|
||||||
platform: PlatformUtils.detectPlatform(context),
|
|
||||||
sections: [
|
|
||||||
SettingsSection(
|
|
||||||
tiles: values.map((e) {
|
|
||||||
return SettingsTile(
|
|
||||||
leading: icons != null ? icons!(e) : null,
|
|
||||||
title: Text(e.toString()),
|
|
||||||
onPressed: (_) {
|
|
||||||
onValueChanged(e);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,245 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:settings_ui/settings_ui.dart';
|
|
||||||
import 'package:simplecloudnotifier/git_stamp/git_stamp.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/settings/settings_number_modal.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/settings/settings_picker_screen.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
|
|
||||||
class SettingsRootPage extends StatefulWidget {
|
|
||||||
const SettingsRootPage({super.key, required bool isVisiblePage});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingsRootPageState extends State<SettingsRootPage> {
|
|
||||||
int _multiClickCounter = 0;
|
|
||||||
DateTime? _lastClickTime = null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final cfg = Provider.of<AppSettings>(context);
|
|
||||||
final thm = Provider.of<AppTheme>(context);
|
|
||||||
|
|
||||||
return SettingsList(
|
|
||||||
platform: PlatformUtils.detectPlatform(context),
|
|
||||||
contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 24),
|
|
||||||
sections: [
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('General'),
|
|
||||||
tiles: [
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(thm.darkMode ? FontAwesomeIcons.solidMoon : FontAwesomeIcons.solidSun),
|
|
||||||
title: Text('Theme'),
|
|
||||||
value: Text(thm.darkMode ? 'Dark' : 'Light'),
|
|
||||||
onPressed: (_) => thm.switchDarkMode(),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidSquare, color: thm.color.value),
|
|
||||||
title: Text('Color'),
|
|
||||||
value: Text(thm.color.displayStr),
|
|
||||||
onPressed: (_) => Navi.push(
|
|
||||||
context,
|
|
||||||
() => SettingsPickerScreen(
|
|
||||||
title: 'Color',
|
|
||||||
initialValue: thm.color,
|
|
||||||
values: ThemeColor.values,
|
|
||||||
icons: (v) => Icon(FontAwesomeIcons.solidSquare, color: v.value),
|
|
||||||
onValueChanged: (value) => AppTheme().setColor(value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidLineColumns),
|
|
||||||
title: Text('Message Preview Lines'),
|
|
||||||
value: Text("${cfg.messagePreviewLength}"),
|
|
||||||
onPressed: (_) {
|
|
||||||
SettingsNumberModal.show(
|
|
||||||
context,
|
|
||||||
title: 'Message Preview Lines',
|
|
||||||
currentValue: cfg.messagePreviewLength,
|
|
||||||
minValue: 1,
|
|
||||||
maxValue: 32,
|
|
||||||
onValueChanged: (value) => AppSettings().update((p) => p.messagePreviewLength = value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (Platform.isAndroid)
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: cfg.groupNotifications,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidLayerGroup),
|
|
||||||
title: Text('Group notifications together'),
|
|
||||||
onToggle: (value) => AppSettings().update((p) => p.groupNotifications = !p.groupNotifications),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidCalendarDays),
|
|
||||||
title: Text('Date Format'),
|
|
||||||
value: Text(cfg.dateFormat.displayStr),
|
|
||||||
onPressed: (_) => Navi.push(
|
|
||||||
context,
|
|
||||||
() => SettingsPickerScreen(
|
|
||||||
title: 'Date Format',
|
|
||||||
initialValue: cfg.dateFormat,
|
|
||||||
values: AppSettingsDateFormat.values,
|
|
||||||
onValueChanged: (value) => AppSettings().update((p) => p.dateFormat = value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('Priority 0 (Low)'),
|
|
||||||
tiles: _buildNotificationTiles(context, cfg, 0),
|
|
||||||
),
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('Priority 1 (Normal)'),
|
|
||||||
tiles: _buildNotificationTiles(context, cfg, 1),
|
|
||||||
),
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('Priority 2 (High)'),
|
|
||||||
tiles: _buildNotificationTiles(context, cfg, 2),
|
|
||||||
),
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('Advanced Settings'),
|
|
||||||
tiles: [
|
|
||||||
if (cfg.devMode)
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: cfg.showDebugButton,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
|
||||||
title: Text('Debug Button anzeigen'),
|
|
||||||
onToggle: (value) => AppSettings().update((p) => p.showDebugButton = !p.showDebugButton),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidList),
|
|
||||||
title: Text('Page Size (Messages)'),
|
|
||||||
value: Text("${cfg.messagePageSize}"),
|
|
||||||
onPressed: (_) {
|
|
||||||
SettingsNumberModal.show(
|
|
||||||
context,
|
|
||||||
title: 'Page Size (Messages)',
|
|
||||||
currentValue: cfg.messagePageSize,
|
|
||||||
minValue: 1,
|
|
||||||
maxValue: 2048,
|
|
||||||
onValueChanged: (value) => AppSettings().update((p) => p.messagePageSize = value),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: cfg.backgroundRefreshMessageListOnPop,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidPageCaretDown),
|
|
||||||
title: Text('Refresh messages on page navigation'),
|
|
||||||
onToggle: (value) => AppSettings().update((p) => p.backgroundRefreshMessageListOnPop = !p.backgroundRefreshMessageListOnPop),
|
|
||||||
),
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: cfg.alwaysBackgroundRefreshMessageListOnLifecycleResume,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidRecycle),
|
|
||||||
title: Text('Refresh messages on app resume'),
|
|
||||||
onToggle: (value) => AppSettings().update((p) => p.alwaysBackgroundRefreshMessageListOnLifecycleResume = !p.alwaysBackgroundRefreshMessageListOnLifecycleResume),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SettingsSection(
|
|
||||||
title: Text('About'),
|
|
||||||
tiles: [
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidCodeCommit),
|
|
||||||
title: Text('Version'),
|
|
||||||
value: Text(Globals().version),
|
|
||||||
onPressed: (cfg.devMode)
|
|
||||||
? null
|
|
||||||
: (context) {
|
|
||||||
if (_lastClickTime == null || DateTime.now().difference(_lastClickTime!).inSeconds > 1) _multiClickCounter = 0;
|
|
||||||
_multiClickCounter++;
|
|
||||||
_lastClickTime = DateTime.now();
|
|
||||||
|
|
||||||
if (_multiClickCounter >= 12) {
|
|
||||||
Toaster.info("Debug", "Developer mode enabled");
|
|
||||||
AppSettings().update((p) {
|
|
||||||
p.devMode = true;
|
|
||||||
p.showDebugButton = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidCodeBranch),
|
|
||||||
title: Text('Build'),
|
|
||||||
value: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(GitStamp.sha.substring(0, 7) + ' +' + Globals().buildNumber),
|
|
||||||
Text("( " + cfg.dateFormat.dateFormat().format(DateTime.parse(GitStamp.buildDateTime).toLocal()) + " )", style: TextStyle(fontStyle: FontStyle.italic)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: (context) => _clipboardCopy(GitStamp.sha),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidBell),
|
|
||||||
title: Text('FCM Token'),
|
|
||||||
value: Text(AppAuth().getToken()),
|
|
||||||
onPressed: (context) => _clipboardCopy(AppAuth().getToken()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _clipboardCopy(String v) {
|
|
||||||
Clipboard.setData(new ClipboardData(text: v));
|
|
||||||
Toaster.info("Clipboard", 'Copied to Clipboard');
|
|
||||||
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AbstractSettingsTile> _buildNotificationTiles(BuildContext context, AppSettings cfg, int prio) {
|
|
||||||
final ncf = AppSettings().getNotificationSettings(prio);
|
|
||||||
return [
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: ncf.enableLights,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidLightbulb),
|
|
||||||
title: Text('Enable Lights'),
|
|
||||||
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withEnableLights(!p.enableLights)),
|
|
||||||
),
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: ncf.enableVibration,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidShutters),
|
|
||||||
title: Text('Enable Vibration'),
|
|
||||||
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withEnableVibration(!p.enableVibration)),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidWaveform),
|
|
||||||
title: Text('Notification Sound'),
|
|
||||||
value: Text(ncf.sound ?? '(Default)'),
|
|
||||||
onPressed: (context) => {/*TODO*/},
|
|
||||||
),
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: ncf.playSound,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidVolume),
|
|
||||||
title: Text('Play Sound'),
|
|
||||||
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withPlaySound(!p.playSound)),
|
|
||||||
),
|
|
||||||
SettingsTile.switchTile(
|
|
||||||
initialValue: ncf.silent,
|
|
||||||
leading: Icon(FontAwesomeIcons.solidVolumeSlash),
|
|
||||||
title: Text('Silent'),
|
|
||||||
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withSilent(!p.silent)),
|
|
||||||
),
|
|
||||||
SettingsTile.navigation(
|
|
||||||
leading: Icon(FontAwesomeIcons.solidStopwatch20),
|
|
||||||
title: Text('Auto Timeout'),
|
|
||||||
value: Text((ncf.timeoutAfter != null) ? "${ncf.timeoutAfter} sec" : "(None)"),
|
|
||||||
onPressed: (context) => {/*TODO*/},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
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/models/subscription.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/subscription_list/subscription_list_item.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
|
||||||
|
|
||||||
class SubscriptionListPage extends StatefulWidget {
|
|
||||||
const SubscriptionListPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SubscriptionListPage> createState() => _SubscriptionListPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SubscriptionListPageState extends State<SubscriptionListPage> {
|
|
||||||
final PagingController<int, Subscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
|
||||||
|
|
||||||
final userCache = Map<String, UserPreview>();
|
|
||||||
final channelCache = Map<String, ChannelPreview>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
for (var v in SCNDataCache().getChannelMap().entries) channelCache[v.key] = v.value.toPreview(null);
|
|
||||||
|
|
||||||
_pagingController.addPageRequestListener(_fetchPage);
|
|
||||||
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
ApplicationLog.debug('SubscriptionListPage::dispose');
|
|
||||||
_pagingController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPage(int pageKey) async {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
ApplicationLog.debug('Start SubscriptionListPage::_pagingController::_fetchPage [ ${pageKey} ]');
|
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
|
||||||
_pagingController.error = 'Not logged in';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final items = (await APIClient.getSubscriptionList(acc)).toList();
|
|
||||||
|
|
||||||
items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated));
|
|
||||||
|
|
||||||
var promises = Map<String, Future<UserPreview>>();
|
|
||||||
|
|
||||||
for (var item in items) {
|
|
||||||
if (userCache[item.subscriberUserID] == null && !promises.containsKey(item.subscriberUserID)) {
|
|
||||||
promises[item.subscriberUserID] = APIClient.getUserPreview(acc, item.subscriberUserID).then((p) => userCache[p.userID] = p);
|
|
||||||
}
|
|
||||||
if (userCache[item.channelOwnerUserID] == null && !promises.containsKey(item.channelOwnerUserID)) {
|
|
||||||
promises[item.channelOwnerUserID] = APIClient.getUserPreview(acc, item.channelOwnerUserID).then((p) => userCache[p.userID] = p);
|
|
||||||
}
|
|
||||||
if (channelCache[item.channelID] == null && !promises.containsKey(item.channelID)) {
|
|
||||||
channelCache[item.channelID] = await APIClient.getChannelPreview(acc, item.channelID).then((p) => channelCache[p.channelID] = p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Future.wait(promises.values);
|
|
||||||
|
|
||||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
_pagingController.error = exc.toString();
|
|
||||||
ApplicationLog.error('Failed to list subscriptions: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SCNScaffold(
|
|
||||||
title: "Subscriptions",
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
|
||||||
child: RefreshIndicator(
|
|
||||||
onRefresh: () => Future.sync(
|
|
||||||
() => _pagingController.refresh(),
|
|
||||||
),
|
|
||||||
child: PagedListView<int, Subscription>(
|
|
||||||
pagingController: _pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate<Subscription>(
|
|
||||||
itemBuilder: (context, item, index) => SubscriptionListItem(item: item, userCache: userCache, channelCache: channelCache, needsReload: fullRefresh),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fullRefresh() {
|
|
||||||
ApplicationLog.debug('SubscriptionListPage::fullRefresh');
|
|
||||||
_pagingController.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/subscription_view/subscription_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
|
|
||||||
enum SubscriptionListItemMode {
|
|
||||||
Messages,
|
|
||||||
Extended,
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubscriptionListItem extends StatelessWidget {
|
|
||||||
const SubscriptionListItem({
|
|
||||||
required this.item,
|
|
||||||
required this.userCache,
|
|
||||||
required this.channelCache,
|
|
||||||
required this.needsReload,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Subscription item;
|
|
||||||
final Map<String, UserPreview> userCache;
|
|
||||||
final Map<String, ChannelPreview> channelCache;
|
|
||||||
|
|
||||||
final void Function()? needsReload;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final channelOwner = userCache[item.channelOwnerUserID];
|
|
||||||
final subscriber = userCache[item.subscriberUserID];
|
|
||||||
final channel = channelCache[item.channelID];
|
|
||||||
|
|
||||||
return Card.filled(
|
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
|
||||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
|
||||||
color: Theme.of(context).cardTheme.color,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
Navi.push(context, () => SubscriptionViewPage(subscriptionID: item.subscriptionID, preloadedData: (item, channelOwner, subscriber, channel), needsReload: this.needsReload));
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(FontAwesomeIcons.solidDiagramSubtask, color: Theme.of(context).colorScheme.outline, size: 32),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
subscriber?.username ?? item.subscriberUserID,
|
|
||||||
style: const TextStyle(),
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"@" + (channel?.displayName ?? item.channelID),
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Text(
|
|
||||||
"(" + (channelOwner?.username ?? item.channelOwnerUserID) + ")",
|
|
||||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: _buildIcon(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIcon(BuildContext context) {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
final colorFull = Theme.of(context).colorScheme.onPrimaryContainer;
|
|
||||||
final colorHalf = Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(75);
|
|
||||||
|
|
||||||
final isOwned = item.channelOwnerUserID == acc.userID && item.subscriberUserID == acc.userID;
|
|
||||||
final isIncoming = item.channelOwnerUserID == acc.userID && item.subscriberUserID != acc.userID;
|
|
||||||
final isOutgoing = item.channelOwnerUserID != acc.userID && item.subscriberUserID == acc.userID;
|
|
||||||
|
|
||||||
if (isOutgoing && !item.confirmed) return Icon(FontAwesomeIcons.solidSquareEnvelope, color: colorHalf, size: 24);
|
|
||||||
if (isOutgoing && !item.active) return Icon(FontAwesomeIcons.solidSquareShareNodes, color: colorHalf, size: 24);
|
|
||||||
if (isOutgoing && item.active) return Icon(FontAwesomeIcons.solidSquareShareNodes, color: colorFull, size: 24);
|
|
||||||
|
|
||||||
if (isIncoming && !item.confirmed) return Icon(FontAwesomeIcons.solidSquareQuestion, color: colorHalf, size: 24);
|
|
||||||
if (isIncoming && !item.active) return Icon(FontAwesomeIcons.solidSquareCheck, color: colorHalf, size: 24);
|
|
||||||
if (isIncoming && item.active) return Icon(FontAwesomeIcons.solidSquareCheck, color: colorFull, size: 24);
|
|
||||||
|
|
||||||
if (isOwned && !item.confirmed) return Icon(FontAwesomeIcons.solidSquare, color: colorHalf, size: 24); // should not be possible
|
|
||||||
if (isOwned && !item.active) return Icon(FontAwesomeIcons.solidSquareRss, color: colorHalf, size: 24);
|
|
||||||
if (isOwned && item.active) return Icon(FontAwesomeIcons.solidSquareRss, color: colorFull, size: 24);
|
|
||||||
|
|
||||||
return SizedBox(width: 24, height: 24); // should also not be possible
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,474 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
|
||||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/dialogs.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class SubscriptionViewPage extends StatefulWidget {
|
|
||||||
const SubscriptionViewPage({
|
|
||||||
required this.subscriptionID,
|
|
||||||
required this.preloadedData,
|
|
||||||
required this.needsReload,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String subscriptionID;
|
|
||||||
final (Subscription?, UserPreview?, UserPreview?, ChannelPreview?)? preloadedData;
|
|
||||||
|
|
||||||
final void Function()? needsReload;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SubscriptionViewPage> createState() => _SubscriptionViewPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
enum EditState { none, editing, saving }
|
|
||||||
|
|
||||||
enum SubscriptionViewPageInitState { loading, okay, error }
|
|
||||||
|
|
||||||
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
|
|
||||||
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
|
|
||||||
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
|
|
||||||
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
|
|
||||||
|
|
||||||
int _loadingIndeterminateCounter = 0;
|
|
||||||
|
|
||||||
Subscription? subscription;
|
|
||||||
|
|
||||||
SubscriptionViewPageInitState loadingState = SubscriptionViewPageInitState.loading;
|
|
||||||
String errorMessage = '';
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
_initStateAsync(true);
|
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initStateAsync(bool usePreload) async {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
if (widget.preloadedData?.$1 != null && widget.preloadedData!.$1!.subscriptionID == widget.subscriptionID && usePreload) {
|
|
||||||
subscription = widget.preloadedData!.$1!;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
var r = await APIClient.getSubscription(userAcc, widget.subscriptionID);
|
|
||||||
setState(() {
|
|
||||||
subscription = r;
|
|
||||||
});
|
|
||||||
} catch (exc, trace) {
|
|
||||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
|
||||||
Toaster.error("Error", 'Failed to load data');
|
|
||||||
this.errorMessage = 'Failed to load data: ' + exc.toString();
|
|
||||||
this.loadingState = SubscriptionViewPageInitState.error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
this.loadingState = SubscriptionViewPageInitState.okay;
|
|
||||||
|
|
||||||
assert(subscription != null);
|
|
||||||
|
|
||||||
if (widget.preloadedData?.$2 != null && widget.preloadedData!.$2!.userID == this.subscription!.channelOwnerUserID && usePreload) {
|
|
||||||
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$2!);
|
|
||||||
} else if (widget.preloadedData?.$3 != null && widget.preloadedData!.$3!.userID == this.subscription!.channelOwnerUserID && usePreload) {
|
|
||||||
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$3!);
|
|
||||||
} else if (this.subscription!.channelOwnerUserID == userAcc.userID) {
|
|
||||||
var cacheUser = userAcc.getUserOrNull();
|
|
||||||
if (cacheUser != null) {
|
|
||||||
_futureChannelOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
|
||||||
} else {
|
|
||||||
_futureChannelOwner = ImmediateFuture<UserPreview>.ofFuture(_getUserPreview(userAcc, this.subscription!.channelOwnerUserID));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_futureChannelOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.subscription!.channelOwnerUserID));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.preloadedData?.$2 != null && widget.preloadedData!.$2!.userID == this.subscription!.subscriberUserID && usePreload) {
|
|
||||||
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$2!);
|
|
||||||
} else if (widget.preloadedData?.$3 != null && widget.preloadedData!.$3!.userID == this.subscription!.subscriberUserID && usePreload) {
|
|
||||||
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(widget.preloadedData!.$3!);
|
|
||||||
} else if (this.subscription!.subscriberUserID == userAcc.userID) {
|
|
||||||
var cacheUser = userAcc.getUserOrNull();
|
|
||||||
if (cacheUser != null) {
|
|
||||||
_futureSubscriber = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
|
||||||
} else {
|
|
||||||
_futureSubscriber = ImmediateFuture<UserPreview>.ofFuture(_getUserPreview(userAcc, this.subscription!.subscriberUserID));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_futureSubscriber = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.subscription!.subscriberUserID));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.preloadedData?.$4 != null && widget.preloadedData!.$4!.channelID == this.subscription!.channelID && usePreload) {
|
|
||||||
_futureChannel = ImmediateFuture<ChannelPreview>.ofValue(widget.preloadedData!.$4!);
|
|
||||||
} else {
|
|
||||||
_futureChannel = ImmediateFuture<ChannelPreview>.ofFuture(APIClient.getChannelPreview(userAcc, this.subscription!.channelID));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
Widget child;
|
|
||||||
|
|
||||||
if (loadingState == SubscriptionViewPageInitState.loading) {
|
|
||||||
child = Center(child: CircularProgressIndicator());
|
|
||||||
} else if (loadingState == SubscriptionViewPageInitState.error) {
|
|
||||||
child = ErrorDisplay(errorMessage: errorMessage);
|
|
||||||
} else if (loadingState == SubscriptionViewPageInitState.okay) {
|
|
||||||
if (subscription!.channelOwnerUserID == userAcc.userID && subscription!.subscriberUserID == userAcc.userID) {
|
|
||||||
child = _buildOwnedSubscriptionView(context, this.subscription!);
|
|
||||||
} else if (subscription!.channelOwnerUserID == userAcc.userID) {
|
|
||||||
child = _buildIncomingSubscriptionView(context, this.subscription!);
|
|
||||||
} else if (subscription!.subscriberUserID == userAcc.userID) {
|
|
||||||
child = _buildOutgoingSubscriptionView(context, this.subscription!);
|
|
||||||
} else {
|
|
||||||
child = ErrorDisplay(errorMessage: 'Invalid subscription state!');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
child = ErrorDisplay(errorMessage: 'Invalid page state!');
|
|
||||||
}
|
|
||||||
|
|
||||||
return SCNScaffold(
|
|
||||||
title: "Subscription",
|
|
||||||
showSearch: false,
|
|
||||||
showShare: false,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 8),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'SubscriptionID',
|
|
||||||
values: [subscription.subscriptionID],
|
|
||||||
),
|
|
||||||
_buildChannelOwnerCard(context, subscription),
|
|
||||||
_buildSubscriberCard(context, subscription),
|
|
||||||
_buildChannelCard(context, subscription),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.clock,
|
|
||||||
title: 'Created',
|
|
||||||
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
|
|
||||||
),
|
|
||||||
_buildStatusCard(context),
|
|
||||||
UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 8),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'SubscriptionID',
|
|
||||||
values: [subscription.subscriptionID],
|
|
||||||
),
|
|
||||||
_buildChannelOwnerCard(context, subscription),
|
|
||||||
_buildSubscriberCard(context, subscription),
|
|
||||||
_buildChannelCard(context, subscription),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.clock,
|
|
||||||
title: 'Created',
|
|
||||||
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
|
|
||||||
),
|
|
||||||
_buildStatusCard(context),
|
|
||||||
if (subscription.confirmed) UI.button(text: "Revoke subscription", onPressed: _unsubscribe, color: Colors.red),
|
|
||||||
if (!subscription.confirmed) UI.button(text: "Confirm subscription", onPressed: _confirm, color: Colors.green),
|
|
||||||
if (!subscription.confirmed) UI.button(text: "Deny subscription", onPressed: _unsubscribe, color: Colors.red),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOutgoingSubscriptionView(BuildContext context, Subscription subscription) {
|
|
||||||
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateOnlyFormat();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
SizedBox(height: 8),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
|
||||||
title: 'SubscriptionID',
|
|
||||||
values: [subscription.subscriptionID],
|
|
||||||
),
|
|
||||||
_buildChannelOwnerCard(context, subscription),
|
|
||||||
_buildSubscriberCard(context, subscription),
|
|
||||||
_buildChannelCard(context, subscription),
|
|
||||||
UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.clock,
|
|
||||||
title: 'Created',
|
|
||||||
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
|
|
||||||
),
|
|
||||||
_buildStatusCard(context),
|
|
||||||
if (subscription.confirmed && subscription.active) UI.button(text: "Deactivate subscription", onPressed: _deactivate, tonal: true),
|
|
||||||
if (subscription.confirmed && !subscription.active) UI.button(text: "Activate subscription", onPressed: _activate, tonal: true),
|
|
||||||
if (subscription.confirmed && !subscription.active) UI.button(text: "Delete subscription", onPressed: () => _unsubscribe(confirm: 'Really (permanently) delete the subscription to this channel?'), color: Colors.red),
|
|
||||||
if (!subscription.confirmed) UI.button(text: "Cancel subscription request", onPressed: _unsubscribe, tonal: true),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildChannelOwnerCard(BuildContext context, Subscription subscription) {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
bool isSelf = subscription.channelOwnerUserID == userAcc.userID;
|
|
||||||
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _futureChannelOwner.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Channel Owner',
|
|
||||||
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Channel Owner',
|
|
||||||
values: [subscription.channelOwnerUserID + (isSelf ? ' (you)' : '')],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubscriberCard(BuildContext context, Subscription subscription) {
|
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
bool isSelf = subscription.subscriberUserID == userAcc.userID;
|
|
||||||
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _futureSubscriber.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Subscriber',
|
|
||||||
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidUser,
|
|
||||||
title: 'Subscriber',
|
|
||||||
values: [subscription.subscriberUserID + (isSelf ? ' (you)' : '')],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildChannelCard(BuildContext context, Subscription subscription) {
|
|
||||||
return FutureBuilder(
|
|
||||||
future: _futureChannel.future,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channel',
|
|
||||||
values: [subscription.channelID, snapshot.data!.displayName],
|
|
||||||
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
|
||||||
title: 'Channel',
|
|
||||||
values: [subscription.channelID, subscription.channelInternalName],
|
|
||||||
mainAction: () => Navi.push(context, () => ChannelViewPage(channelID: subscription.channelID, preloadedData: null, needsReload: null)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatusCard(BuildContext context) {
|
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
final item = subscription!;
|
|
||||||
|
|
||||||
final isOwned = item.channelOwnerUserID == acc.userID && item.subscriberUserID == acc.userID;
|
|
||||||
final isIncoming = item.channelOwnerUserID == acc.userID && item.subscriberUserID != acc.userID;
|
|
||||||
final isOutgoing = item.channelOwnerUserID != acc.userID && item.subscriberUserID == acc.userID;
|
|
||||||
|
|
||||||
var status = ['ERROR?'];
|
|
||||||
|
|
||||||
if (isOutgoing && !item.confirmed) status = ['Subscription to foreign channel', 'Pending confirmation'];
|
|
||||||
if (isOutgoing && !item.active) status = ['Subscription to foreign channel', 'Confirmed but inactive'];
|
|
||||||
if (isOutgoing && item.active) status = ['Subscription to foreign channel', 'Confirmed and active'];
|
|
||||||
|
|
||||||
if (isIncoming && !item.confirmed) status = ['External subscription to your channel', 'Pending confirmation'];
|
|
||||||
if (isIncoming && !item.active) status = ['External subscription to your channel', 'Deactivated by subscriber'];
|
|
||||||
if (isIncoming && item.active) status = ['External subscription to your channel', 'Confirmed and active'];
|
|
||||||
|
|
||||||
if (isOwned && !item.confirmed) status = ['Your own channel', 'ERROR'];
|
|
||||||
if (isOwned && !item.active) status = ['Your own channel', 'Not subscribed'];
|
|
||||||
if (isOwned && item.active) status = ['Your own channel', 'Active subscription'];
|
|
||||||
|
|
||||||
return UI.metaCard(
|
|
||||||
context: context,
|
|
||||||
icon: FontAwesomeIcons.solidInfo,
|
|
||||||
title: 'Status',
|
|
||||||
values: status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<UserPreview> _getUserPreview(AppAuth auth, String uid) async {
|
|
||||||
try {
|
|
||||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
|
||||||
|
|
||||||
_incLoadingIndeterminateCounter(1);
|
|
||||||
|
|
||||||
final owner = APIClient.getUserPreview(auth, uid);
|
|
||||||
|
|
||||||
//await Future.delayed(const Duration(seconds: 10), () {});
|
|
||||||
|
|
||||||
return owner;
|
|
||||||
} finally {
|
|
||||||
_incLoadingIndeterminateCounter(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _incLoadingIndeterminateCounter(int delta) {
|
|
||||||
setState(() {
|
|
||||||
_loadingIndeterminateCounter += delta;
|
|
||||||
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirm() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.confirmSubscription(acc, subscription!.channelID, subscription!.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 _unsubscribe({String? confirm = null}) async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
if (confirm != null) {
|
|
||||||
final r = await UIDialogs.showConfirmDialog(context, confirm, okText: 'Unsubscribe', cancelText: 'Cancel');
|
|
||||||
if (!r) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.deleteSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
|
|
||||||
widget.needsReload?.call();
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Unsubscribed from channel');
|
|
||||||
Navi.pop(context);
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
|
||||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deactivate() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.deactivateSubscription(acc, subscription!.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 _activate() async {
|
|
||||||
final acc = AppAuth();
|
|
||||||
|
|
||||||
if (subscription == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await APIClient.activateSubscription(acc, subscription!.channelID, subscription!.subscriptionID);
|
|
||||||
widget.needsReload?.call();
|
|
||||||
|
|
||||||
await _initStateAsync(false);
|
|
||||||
|
|
||||||
Toaster.success("Success", 'Subscribed to channel');
|
|
||||||
} catch (exc, trace) {
|
|
||||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
|
||||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
35
flutter/lib/settings/app_settings.dart
Normal file
35
flutter/lib/settings/app_settings.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppSettings extends ChangeNotifier {
|
||||||
|
bool groupNotifications = true;
|
||||||
|
int messagePageSize = 128;
|
||||||
|
bool showDebugButton = true;
|
||||||
|
bool backgroundRefreshMessageListOnPop = false;
|
||||||
|
bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
|
||||||
|
|
||||||
|
static AppSettings? _singleton = AppSettings._internal();
|
||||||
|
|
||||||
|
factory AppSettings() {
|
||||||
|
return _singleton ?? (_singleton = AppSettings._internal());
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings._internal() {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void load() {
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> save() async {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isAuth() {
|
bool isAuth() {
|
||||||
return _userID != null && _tokenAdmin != null && _tokenSend != null;
|
return _userID != null && _tokenAdmin != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(User user, Client client, String tokenAdmin, String tokenSend) {
|
void set(User user, Client client, String tokenAdmin, String tokenSend) {
|
||||||
@ -229,8 +229,4 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
|||||||
String getUserID() {
|
String getUserID() {
|
||||||
return _userID!;
|
return _userID!;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getClientID() {
|
|
||||||
return _clientID;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:shared_preferences/src/shared_preferences_legacy.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
|
||||||
|
|
||||||
enum AppSettingsDateFormat {
|
|
||||||
ISO(displayStr: 'ISO (yyyy-MM-dd)', key: 'ISO'),
|
|
||||||
German(displayStr: 'German (dd.MM.yyyy)', key: 'German'),
|
|
||||||
US(displayStr: 'US (MM/dd/yyyy)', key: 'US');
|
|
||||||
|
|
||||||
const AppSettingsDateFormat({required this.displayStr, required this.key});
|
|
||||||
|
|
||||||
final String displayStr;
|
|
||||||
final String key;
|
|
||||||
|
|
||||||
@override
|
|
||||||
toString() => displayStr;
|
|
||||||
|
|
||||||
DateFormat dateFormat() {
|
|
||||||
switch (this) {
|
|
||||||
case AppSettingsDateFormat.ISO:
|
|
||||||
return DateFormat('yyyy-MM-dd HH:mm');
|
|
||||||
case AppSettingsDateFormat.German:
|
|
||||||
return DateFormat('dd.MM.yyyy HH:mm');
|
|
||||||
case AppSettingsDateFormat.US:
|
|
||||||
return DateFormat('MM/dd/yyyy HH:mm');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateFormat dateOnlyFormat() {
|
|
||||||
switch (this) {
|
|
||||||
case AppSettingsDateFormat.ISO:
|
|
||||||
return DateFormat('yyyy-MM-dd');
|
|
||||||
case AppSettingsDateFormat.German:
|
|
||||||
return DateFormat('dd.MM.yyyy');
|
|
||||||
case AppSettingsDateFormat.US:
|
|
||||||
return DateFormat('MM/dd/yyyy');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static AppSettingsDateFormat? parse(String? string) {
|
|
||||||
if (string == null) return null;
|
|
||||||
return values.firstWhere((e) => e.key == string, orElse: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppSettings extends ChangeNotifier {
|
|
||||||
bool groupNotifications = true;
|
|
||||||
int messagePageSize = 128;
|
|
||||||
bool devMode = false;
|
|
||||||
bool showDebugButton = false;
|
|
||||||
bool backgroundRefreshMessageListOnPop = false;
|
|
||||||
bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
|
|
||||||
AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO;
|
|
||||||
int messagePreviewLength = 3;
|
|
||||||
|
|
||||||
AppNotificationSettings notification0 = AppNotificationSettings();
|
|
||||||
AppNotificationSettings notification1 = AppNotificationSettings();
|
|
||||||
AppNotificationSettings notification2 = AppNotificationSettings();
|
|
||||||
|
|
||||||
static AppSettings? _singleton = AppSettings._internal();
|
|
||||||
|
|
||||||
factory AppSettings() {
|
|
||||||
return _singleton ?? (_singleton = AppSettings._internal());
|
|
||||||
}
|
|
||||||
|
|
||||||
AppSettings._internal() {
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
groupNotifications = true;
|
|
||||||
messagePageSize = 128;
|
|
||||||
devMode = false;
|
|
||||||
showDebugButton = false;
|
|
||||||
backgroundRefreshMessageListOnPop = false;
|
|
||||||
alwaysBackgroundRefreshMessageListOnLifecycleResume = true;
|
|
||||||
dateFormat = AppSettingsDateFormat.ISO;
|
|
||||||
messagePreviewLength = 3;
|
|
||||||
|
|
||||||
notification0 = AppNotificationSettings();
|
|
||||||
notification1 = AppNotificationSettings();
|
|
||||||
notification2 = AppNotificationSettings();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void load() {
|
|
||||||
groupNotifications = Globals().sharedPrefs.getBool('settings.groupNotifications') ?? groupNotifications;
|
|
||||||
messagePageSize = Globals().sharedPrefs.getInt('settings.messagePageSize') ?? messagePageSize;
|
|
||||||
devMode = Globals().sharedPrefs.getBool('settings.devMode') ?? devMode;
|
|
||||||
showDebugButton = Globals().sharedPrefs.getBool('settings.showDebugButton') ?? showDebugButton;
|
|
||||||
backgroundRefreshMessageListOnPop = Globals().sharedPrefs.getBool('settings.backgroundRefreshMessageListOnPop') ?? backgroundRefreshMessageListOnPop;
|
|
||||||
alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume;
|
|
||||||
dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat;
|
|
||||||
messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength;
|
|
||||||
|
|
||||||
notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0');
|
|
||||||
notification1 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification1');
|
|
||||||
notification2 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification2');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> save() async {
|
|
||||||
await Globals().sharedPrefs.setBool('settings.groupNotifications', groupNotifications);
|
|
||||||
await Globals().sharedPrefs.setInt('settings.messagePageSize', messagePageSize);
|
|
||||||
await Globals().sharedPrefs.setBool('settings.devMode', devMode);
|
|
||||||
await Globals().sharedPrefs.setBool('settings.showDebugButton', showDebugButton);
|
|
||||||
await Globals().sharedPrefs.setBool('settings.backgroundRefreshMessageListOnPop', backgroundRefreshMessageListOnPop);
|
|
||||||
await Globals().sharedPrefs.setBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume', alwaysBackgroundRefreshMessageListOnLifecycleResume);
|
|
||||||
await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key);
|
|
||||||
await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength);
|
|
||||||
|
|
||||||
await notification0.save(Globals().sharedPrefs, 'settings.notification0');
|
|
||||||
await notification1.save(Globals().sharedPrefs, 'settings.notification1');
|
|
||||||
await notification2.save(Globals().sharedPrefs, 'settings.notification2');
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(void Function(AppSettings p) fn) {
|
|
||||||
fn(this);
|
|
||||||
save();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateNotification(int prio, AppNotificationSettings Function(AppNotificationSettings p) fn) {
|
|
||||||
if (prio == 0) {
|
|
||||||
notification0 = fn(notification0);
|
|
||||||
} else if (prio == 1) {
|
|
||||||
notification1 = fn(notification1);
|
|
||||||
} else if (prio == 2) {
|
|
||||||
notification2 = fn(notification2);
|
|
||||||
}
|
|
||||||
|
|
||||||
save();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
AppNotificationSettings getNotificationSettings(int? prio) {
|
|
||||||
if (prio != null && prio == 0) {
|
|
||||||
return notification0;
|
|
||||||
} else if (prio != null && prio == 1) {
|
|
||||||
return notification1;
|
|
||||||
} else if (prio != null && prio == 2) {
|
|
||||||
return notification2;
|
|
||||||
} else {
|
|
||||||
return AppNotificationSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppNotificationSettings {
|
|
||||||
// Immutable
|
|
||||||
AppNotificationSettings({
|
|
||||||
this.enableLights = false,
|
|
||||||
this.enableVibration = true,
|
|
||||||
this.playSound = true,
|
|
||||||
this.sound = null,
|
|
||||||
this.silent = false,
|
|
||||||
this.timeoutAfter = null,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool enableLights;
|
|
||||||
final bool enableVibration;
|
|
||||||
final bool playSound;
|
|
||||||
final String? sound;
|
|
||||||
final bool silent;
|
|
||||||
final int? timeoutAfter;
|
|
||||||
|
|
||||||
Future<void> save(SharedPreferences sharedPrefs, String prefix) async {
|
|
||||||
await Globals().sharedPrefs.setBool('${prefix}.enableLights', enableLights);
|
|
||||||
await Globals().sharedPrefs.setBool('${prefix}.enableVibration', enableVibration);
|
|
||||||
await Globals().sharedPrefs.setBool('${prefix}.playSound', playSound);
|
|
||||||
await Globals().sharedPrefs.setString('${prefix}.sound', _encode(sound));
|
|
||||||
await Globals().sharedPrefs.setBool('${prefix}.silent', silent);
|
|
||||||
await Globals().sharedPrefs.setString('${prefix}.timeoutAfter', _encode(timeoutAfter));
|
|
||||||
}
|
|
||||||
|
|
||||||
UriAndroidNotificationSound? soundURI() {
|
|
||||||
return (sound != null) ? UriAndroidNotificationSound(sound!) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppNotificationSettings withEnableLights(bool v) => AppNotificationSettings(enableLights: v, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: silent, timeoutAfter: timeoutAfter);
|
|
||||||
AppNotificationSettings withEnableVibration(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: v, playSound: playSound, sound: sound, silent: silent, timeoutAfter: timeoutAfter);
|
|
||||||
AppNotificationSettings withPlaySound(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: v, sound: sound, silent: silent, timeoutAfter: timeoutAfter);
|
|
||||||
AppNotificationSettings withSound(String? v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: v, silent: silent, timeoutAfter: timeoutAfter);
|
|
||||||
AppNotificationSettings withSilent(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: v, timeoutAfter: timeoutAfter);
|
|
||||||
AppNotificationSettings withTimeoutAfter(int? v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: silent, timeoutAfter: v);
|
|
||||||
|
|
||||||
static AppNotificationSettings load(SharedPreferences prefs, String prefix) {
|
|
||||||
final def = AppNotificationSettings();
|
|
||||||
|
|
||||||
final enableLights = prefs.getBool('${prefix}.enableLights') ?? def.enableLights;
|
|
||||||
final enableVibration = prefs.getBool('${prefix}.enableVibration') ?? def.enableVibration;
|
|
||||||
final playSound = prefs.getBool('${prefix}.playSound') ?? def.playSound;
|
|
||||||
final sound = _decode(prefs.getString('${prefix}.sound'), def.sound);
|
|
||||||
final silent = prefs.getBool('${prefix}.silent') ?? def.silent;
|
|
||||||
final timeoutAfter = _decode(prefs.getString('${prefix}.timeoutAfter'), def.timeoutAfter);
|
|
||||||
|
|
||||||
return AppNotificationSettings(
|
|
||||||
enableLights: enableLights,
|
|
||||||
enableVibration: enableVibration,
|
|
||||||
playSound: playSound,
|
|
||||||
sound: sound,
|
|
||||||
silent: silent,
|
|
||||||
timeoutAfter: timeoutAfter,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _encode<T>(T v) {
|
|
||||||
return JsonEncoder().convert(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
T _decode<T>(String? v, T fallback) {
|
|
||||||
if (v == null) return fallback;
|
|
||||||
try {
|
|
||||||
return JsonDecoder().convert(v) as T;
|
|
||||||
} catch (_) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +1,16 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
|
||||||
|
|
||||||
enum ThemeColor {
|
|
||||||
Pink(displayStr: 'Pink', key: 'PINK', value: Colors.pink),
|
|
||||||
Red(displayStr: 'Red', key: 'RED', value: Colors.red),
|
|
||||||
DeepOrange(displayStr: 'Deep-Orange', key: 'DEEPORANGE', value: Colors.deepOrange),
|
|
||||||
Orange(displayStr: 'Orange', key: 'ORANGE', value: Colors.orange),
|
|
||||||
Amber(displayStr: 'Amber', key: 'AMBER', value: Colors.amber),
|
|
||||||
Yellow(displayStr: 'Yellow', key: 'YELLOW', value: Colors.yellow),
|
|
||||||
Lime(displayStr: 'Lime', key: 'LIME', value: Colors.lime),
|
|
||||||
LightGreen(displayStr: 'Light-Green', key: 'LIGHTGREEN', value: Colors.lightGreen),
|
|
||||||
Green(displayStr: 'Green', key: 'GREEN', value: Colors.green),
|
|
||||||
Teal(displayStr: 'Teal', key: 'TEAL', value: Colors.teal),
|
|
||||||
Cyan(displayStr: 'Cyan', key: 'CYAN', value: Colors.cyan),
|
|
||||||
LightBlue(displayStr: 'Light-Blue', key: 'LIGHTBLUE', value: Colors.lightBlue),
|
|
||||||
Blue(displayStr: 'Blue', key: 'BLUE', value: Colors.blue),
|
|
||||||
Indigo(displayStr: 'Indigo', key: 'INDIGO', value: Colors.indigo),
|
|
||||||
Purple(displayStr: 'Purple', key: 'PURPLE', value: Colors.purple),
|
|
||||||
DeepPurple(displayStr: 'Deep-Purple', key: 'DEEPPURPLE', value: Colors.deepPurple),
|
|
||||||
BlueGrey(displayStr: 'Blue-Grey', key: 'BLUEGREY', value: Colors.blueGrey),
|
|
||||||
Brown(displayStr: 'Brown', key: 'BROWN', value: Colors.brown),
|
|
||||||
Grey(displayStr: 'Grey', key: 'GREY', value: Colors.grey);
|
|
||||||
|
|
||||||
const ThemeColor({required this.displayStr, required this.key, required this.value});
|
|
||||||
|
|
||||||
final String displayStr;
|
|
||||||
final String key;
|
|
||||||
final Color value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
toString() => displayStr;
|
|
||||||
|
|
||||||
static ThemeColor? parse(String? string) {
|
|
||||||
if (string == null) return null;
|
|
||||||
return values.firstWhere((e) => e.key == string, orElse: null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppTheme extends ChangeNotifier {
|
class AppTheme extends ChangeNotifier {
|
||||||
static AppTheme? _singleton = AppTheme._internal();
|
|
||||||
|
|
||||||
factory AppTheme() {
|
|
||||||
return _singleton ?? (_singleton = AppTheme._internal());
|
|
||||||
}
|
|
||||||
|
|
||||||
AppTheme._internal() {}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
bool _darkmode = false;
|
bool _darkmode = false;
|
||||||
bool get darkMode => _darkmode;
|
bool get darkMode => _darkmode;
|
||||||
|
|
||||||
ThemeColor _color = ThemeColor.Blue;
|
|
||||||
ThemeColor get color => _color;
|
|
||||||
|
|
||||||
void setDarkMode(bool v) {
|
void setDarkMode(bool v) {
|
||||||
_darkmode = v;
|
_darkmode = v;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void switchDarkMode() {
|
void switchDarkMode() {
|
||||||
_darkmode = !_darkmode;
|
_darkmode = !_darkmode;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setColor(ThemeColor v) {
|
|
||||||
_color = v;
|
|
||||||
notifyListeners();
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
void load() {
|
|
||||||
_darkmode = Globals().sharedPrefs.getBool('theme.dark') ?? _darkmode;
|
|
||||||
_color = ThemeColor.parse(Globals().sharedPrefs.getString('theme.color')) ?? _color;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> save() async {
|
|
||||||
await Globals().sharedPrefs.setBool('theme.dark', _darkmode);
|
|
||||||
await Globals().sharedPrefs.setString('theme.color', _color.key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,53 +10,76 @@ import 'package:path/path.dart' as path;
|
|||||||
part 'application_log.g.dart';
|
part 'application_log.g.dart';
|
||||||
|
|
||||||
class ApplicationLog {
|
class ApplicationLog {
|
||||||
static const MAX_SIZE = 2048;
|
//TODO max size, auto clear old
|
||||||
|
|
||||||
static void debug(String message, {String? additional, StackTrace? trace}) {
|
static void debug(String message, {String? additional, StackTrace? trace}) {
|
||||||
(additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}');
|
(additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}');
|
||||||
|
|
||||||
_logToBox(SCNLogLevel.debug, message, additional, trace);
|
if (!Hive.isBoxOpen('scn-logs')) return;
|
||||||
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
|
id: Xid().toString(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
level: SCNLogLevel.debug,
|
||||||
|
message: message,
|
||||||
|
additional: additional ?? '',
|
||||||
|
trace: trace?.toString() ?? '',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void info(String message, {String? additional, StackTrace? trace}) {
|
static void info(String message, {String? additional, StackTrace? trace}) {
|
||||||
(additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}');
|
(additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}');
|
||||||
|
|
||||||
_logToBox(SCNLogLevel.info, message, additional, trace);
|
if (!Hive.isBoxOpen('scn-logs')) return;
|
||||||
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
|
id: Xid().toString(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
level: SCNLogLevel.info,
|
||||||
|
message: message,
|
||||||
|
additional: additional ?? '',
|
||||||
|
trace: trace?.toString() ?? '',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void warn(String message, {String? additional, StackTrace? trace}) {
|
static void warn(String message, {String? additional, StackTrace? trace}) {
|
||||||
(additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}');
|
(additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}');
|
||||||
|
|
||||||
_logToBox(SCNLogLevel.warning, message, additional, trace);
|
if (!Hive.isBoxOpen('scn-logs')) return;
|
||||||
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
|
id: Xid().toString(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
level: SCNLogLevel.warning,
|
||||||
|
message: message,
|
||||||
|
additional: additional ?? '',
|
||||||
|
trace: trace?.toString() ?? '',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void error(String message, {String? additional, StackTrace? trace}) {
|
static void error(String message, {String? additional, StackTrace? trace}) {
|
||||||
(additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}');
|
(additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}');
|
||||||
|
|
||||||
_logToBox(SCNLogLevel.error, message, additional, trace);
|
if (!Hive.isBoxOpen('scn-logs')) return;
|
||||||
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
|
id: Xid().toString(),
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
level: SCNLogLevel.error,
|
||||||
|
message: message,
|
||||||
|
additional: additional ?? '',
|
||||||
|
trace: trace?.toString() ?? '',
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fatal(String message, {String? additional, StackTrace? trace}) {
|
static void fatal(String message, {String? additional, StackTrace? trace}) {
|
||||||
(additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}');
|
(additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}');
|
||||||
|
|
||||||
_logToBox(SCNLogLevel.fatal, message, additional, trace);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _logToBox(SCNLogLevel lvl, String message, String? additional, StackTrace? trace) {
|
|
||||||
if (!Hive.isBoxOpen('scn-logs')) return;
|
if (!Hive.isBoxOpen('scn-logs')) return;
|
||||||
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
final box = Hive.box<SCNLog>('scn-logs');
|
|
||||||
|
|
||||||
box.add(SCNLog(
|
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestamp: DateTime.now(),
|
timestamp: DateTime.now(),
|
||||||
level: lvl,
|
level: SCNLogLevel.fatal,
|
||||||
message: message,
|
message: message,
|
||||||
additional: additional ?? '',
|
additional: additional ?? '',
|
||||||
trace: trace?.toString() ?? '',
|
trace: trace?.toString() ?? '',
|
||||||
));
|
));
|
||||||
|
|
||||||
while (box.length > MAX_SIZE) box.deleteAt(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void writeRawFailure(String message, Map<String, dynamic> extraData) async {
|
static void writeRawFailure(String message, Map<String, dynamic> extraData) async {
|
||||||
|
@ -5,16 +5,10 @@ import 'package:simplecloudnotifier/state/interfaces.dart';
|
|||||||
part 'fb_message.g.dart';
|
part 'fb_message.g.dart';
|
||||||
|
|
||||||
class FBMessageLog {
|
class FBMessageLog {
|
||||||
static const MAX_SIZE = 512;
|
//TODO max size, auto clear old
|
||||||
|
|
||||||
static void insert(RemoteMessage msg) {
|
static void insert(RemoteMessage msg) {
|
||||||
if (!Hive.isBoxOpen('scn-fb-messages')) return;
|
Hive.box<FBMessage>('scn-fb-messages').add(FBMessage.fromRemoteMessage(msg));
|
||||||
|
|
||||||
final box = Hive.box<FBMessage>('scn-fb-messages');
|
|
||||||
|
|
||||||
box.add(FBMessage.fromRemoteMessage(msg));
|
|
||||||
|
|
||||||
while (box.length > MAX_SIZE) box.deleteAt(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ class Globals {
|
|||||||
String hostname = '';
|
String hostname = '';
|
||||||
String clientType = '';
|
String clientType = '';
|
||||||
String deviceModel = '';
|
String deviceModel = '';
|
||||||
String deviceName = '';
|
|
||||||
|
|
||||||
late SharedPreferences sharedPrefs;
|
late SharedPreferences sharedPrefs;
|
||||||
|
|
||||||
@ -49,23 +48,18 @@ class Globals {
|
|||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
this.clientType = 'ANDROID';
|
this.clientType = 'ANDROID';
|
||||||
this.deviceModel = (await DeviceInfoPlugin().androidInfo).model;
|
this.deviceModel = (await DeviceInfoPlugin().androidInfo).model;
|
||||||
this.deviceName = (await DeviceInfoPlugin().androidInfo).name;
|
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
this.clientType = 'IOS';
|
this.clientType = 'IOS';
|
||||||
this.deviceModel = (await DeviceInfoPlugin().iosInfo).model;
|
this.deviceModel = (await DeviceInfoPlugin().iosInfo).model;
|
||||||
this.deviceName = (await DeviceInfoPlugin().iosInfo).name;
|
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
this.clientType = 'LINUX';
|
this.clientType = 'LINUX';
|
||||||
this.deviceModel = (await DeviceInfoPlugin().linuxInfo).prettyName;
|
this.deviceModel = (await DeviceInfoPlugin().linuxInfo).prettyName;
|
||||||
this.deviceName = (await DeviceInfoPlugin().linuxInfo).name;
|
|
||||||
} else if (Platform.isWindows) {
|
} else if (Platform.isWindows) {
|
||||||
this.clientType = 'WINDOWS';
|
this.clientType = 'WINDOWS';
|
||||||
this.deviceModel = (await DeviceInfoPlugin().windowsInfo).productName;
|
this.deviceModel = (await DeviceInfoPlugin().windowsInfo).productName;
|
||||||
this.deviceName = (await DeviceInfoPlugin().windowsInfo).computerName;
|
|
||||||
} else if (Platform.isMacOS) {
|
} else if (Platform.isMacOS) {
|
||||||
this.clientType = 'MACOS';
|
this.clientType = 'MACOS';
|
||||||
this.deviceModel = (await DeviceInfoPlugin().macOsInfo).model;
|
this.deviceModel = (await DeviceInfoPlugin().macOsInfo).model;
|
||||||
this.deviceName = (await DeviceInfoPlugin().macOsInfo).computerName;
|
|
||||||
} else {
|
} else {
|
||||||
this.clientType = '?';
|
this.clientType = '?';
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ import 'package:xid/xid.dart';
|
|||||||
part 'request_log.g.dart';
|
part 'request_log.g.dart';
|
||||||
|
|
||||||
class RequestLog {
|
class RequestLog {
|
||||||
static const MAX_SIZE = 1024;
|
//TODO max size, auto clear old
|
||||||
|
|
||||||
static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map<String, String> reqheaders, dynamic e, StackTrace trace) {
|
static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map<String, String> reqheaders, dynamic e, StackTrace trace) {
|
||||||
_logToBox(SCNRequest(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestampStart: tStart,
|
timestampStart: tStart,
|
||||||
timestampEnd: DateTime.now(),
|
timestampEnd: DateTime.now(),
|
||||||
@ -28,7 +28,7 @@ class RequestLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void addRequestAPIError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, APIError apierr) {
|
static void addRequestAPIError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, APIError apierr) {
|
||||||
_logToBox(SCNRequest(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestampStart: t0,
|
timestampStart: t0,
|
||||||
timestampEnd: DateTime.now(),
|
timestampEnd: DateTime.now(),
|
||||||
@ -47,7 +47,7 @@ class RequestLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void addRequestErrorStatuscode(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
|
static void addRequestErrorStatuscode(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
|
||||||
_logToBox(SCNRequest(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestampStart: t0,
|
timestampStart: t0,
|
||||||
timestampEnd: DateTime.now(),
|
timestampEnd: DateTime.now(),
|
||||||
@ -66,7 +66,7 @@ class RequestLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void addRequestSuccess(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
|
static void addRequestSuccess(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders) {
|
||||||
_logToBox(SCNRequest(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestampStart: t0,
|
timestampStart: t0,
|
||||||
timestampEnd: DateTime.now(),
|
timestampEnd: DateTime.now(),
|
||||||
@ -85,7 +85,7 @@ class RequestLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void addRequestDecodeError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, Object exc, StackTrace trace) {
|
static void addRequestDecodeError(String name, DateTime t0, String method, Uri uri, String reqbody, Map<String, String> reqheaders, int responseStatusCode, String responseBody, Map<String, String> responseHeaders, Object exc, StackTrace trace) {
|
||||||
_logToBox(SCNRequest(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
timestampStart: t0,
|
timestampStart: t0,
|
||||||
timestampEnd: DateTime.now(),
|
timestampEnd: DateTime.now(),
|
||||||
@ -102,16 +102,6 @@ class RequestLog {
|
|||||||
stackTrace: trace.toString(),
|
stackTrace: trace.toString(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _logToBox(SCNRequest v) {
|
|
||||||
if (!Hive.isBoxOpen('scn-requests')) return;
|
|
||||||
|
|
||||||
final box = Hive.box<SCNRequest>('scn-requests');
|
|
||||||
|
|
||||||
box.add(v);
|
|
||||||
|
|
||||||
while (box.length > MAX_SIZE) box.deleteAt(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiveType(typeId: 100)
|
@HiveType(typeId: 100)
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||||
|
|
||||||
class SCNDataCache {
|
class SCNDataCache {
|
||||||
SCNDataCache._internal();
|
SCNDataCache._internal();
|
||||||
@ -59,21 +57,4 @@ class SCNDataCache {
|
|||||||
|
|
||||||
return cacheMessages;
|
return cacheMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<KeyToken> getOrQueryTokenByValue(String uid, String tokVal) async {
|
|
||||||
final cache = Hive.box<KeyToken>('scn-keytoken-value-cache');
|
|
||||||
|
|
||||||
final cacheVal = cache.get(tokVal);
|
|
||||||
if (cacheVal != null) {
|
|
||||||
print('[SCNDataCache] Found Token(${tokVal}) in cache');
|
|
||||||
return Future.value(cacheVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
final tok = await APIClient.getKeyTokenByToken(uid, tokVal);
|
|
||||||
|
|
||||||
print('[SCNDataCache] Queried Token(${tokVal}) from API');
|
|
||||||
await cache.put(tokVal, tok);
|
|
||||||
|
|
||||||
return tok;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting
|
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting
|
||||||
// This way we can set the ImmediateFuture.value directly and circumvent that.
|
// This way we can set the ImmediateFuture.value directly and circumvent that.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
class ImmediateFuture<T> {
|
class ImmediateFuture<T> {
|
||||||
final Future<T> future;
|
final Future<T> future;
|
||||||
final T? value;
|
final T? value;
|
||||||
@ -22,10 +20,6 @@ class ImmediateFuture<T> {
|
|||||||
: future = Future.value(v),
|
: future = Future.value(v),
|
||||||
value = v;
|
value = v;
|
||||||
|
|
||||||
ImmediateFuture.ofPending()
|
|
||||||
: future = Completer<T>().future,
|
|
||||||
value = null;
|
|
||||||
|
|
||||||
T? get() {
|
T? get() {
|
||||||
return value ?? _futureValue;
|
return value ?? _futureValue;
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> showConfirmDialog(BuildContext context, String title, {String? text, String? okText, String? cancelText}) {
|
|
||||||
return showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(title),
|
|
||||||
content: (text != null) ? Text(text) : null,
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text(cancelText ?? 'Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(okText ?? 'OK'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).then((value) => value ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -13,15 +13,6 @@ class Navi {
|
|||||||
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pushOnRoot<T extends Widget>(BuildContext context, T Function() builder) {
|
|
||||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
|
||||||
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
|
||||||
|
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
|
||||||
|
|
||||||
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void popToRoot(BuildContext context) {
|
static void popToRoot(BuildContext context) {
|
||||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||||
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
||||||
@ -32,10 +23,6 @@ class Navi {
|
|||||||
static void popDialog(BuildContext dialogContext) {
|
static void popDialog(BuildContext dialogContext) {
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pop(BuildContext context) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
|
class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.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';
|
||||||
|
|
||||||
class Notifier {
|
class Notifier {
|
||||||
static void showLocalNotification(String messageID, String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp, int? prio) async {
|
static void showLocalNotification(String messageID, String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async {
|
||||||
final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000;
|
final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000;
|
||||||
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
|
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
|
||||||
|
|
||||||
@ -60,8 +60,6 @@ class Notifier {
|
|||||||
payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n");
|
payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
final cfg = AppSettings().getNotificationSettings(prio);
|
|
||||||
|
|
||||||
// ======== SHOW NOTIFICATION ========
|
// ======== SHOW NOTIFICATION ========
|
||||||
await flutterLocalNotificationsPlugin.show(
|
await flutterLocalNotificationsPlugin.show(
|
||||||
newMessageNID,
|
newMessageNID,
|
||||||
@ -77,12 +75,6 @@ class Notifier {
|
|||||||
when: timestamp?.millisecondsSinceEpoch,
|
when: timestamp?.millisecondsSinceEpoch,
|
||||||
groupKey: channelID,
|
groupKey: channelID,
|
||||||
subText: (channelName == 'main') ? null : channelName,
|
subText: (channelName == 'main') ? null : channelName,
|
||||||
enableLights: cfg.enableLights,
|
|
||||||
enableVibration: cfg.enableVibration,
|
|
||||||
playSound: cfg.playSound,
|
|
||||||
sound: cfg.soundURI(),
|
|
||||||
silent: cfg.silent,
|
|
||||||
timeoutAfter: cfg.timeoutAfter,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
class UI {
|
class UI {
|
||||||
static const double DefaultBorderRadius = 4;
|
static const double DefaultBorderRadius = 4;
|
||||||
|
|
||||||
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, Color? textColor = null, bool tonal = false, IconData? icon = null}) {
|
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, bool tonal = false, IconData? icon = null}) {
|
||||||
final double fontSize = big ? 24 : 14;
|
final double fontSize = big ? 24 : 14;
|
||||||
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ class UI {
|
|||||||
textStyle: TextStyle(fontSize: fontSize),
|
textStyle: TextStyle(fontSize: fontSize),
|
||||||
padding: padding,
|
padding: padding,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
foregroundColor: textColor,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -49,35 +48,19 @@ class UI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget buttonIconOnly({required void Function() onPressed, required IconData icon, double? iconSize = null, bool? square, Color? color = null, Color? iconColor = null}) {
|
static Widget buttonIconOnly({
|
||||||
final style = ButtonStyle(
|
required void Function() onPressed,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
required IconData icon,
|
||||||
backgroundColor: (color != null) ? WidgetStateProperty.resolveWith<Color?>((states) => color) : null,
|
double? iconSize = null,
|
||||||
padding: (square ?? false) ? WidgetStateProperty.resolveWith<EdgeInsetsGeometry?>((states) => EdgeInsets.all(10)) : null,
|
}) {
|
||||||
shape: (square ?? false) ? WidgetStateProperty.resolveWith<OutlinedBorder?>((states) => RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius))) : null,
|
return IconButton(
|
||||||
|
icon: FaIcon(icon),
|
||||||
|
iconSize: iconSize ?? 18,
|
||||||
|
padding: EdgeInsets.all(4),
|
||||||
|
constraints: BoxConstraints(),
|
||||||
|
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap),
|
||||||
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (color != null) {
|
|
||||||
return IconButton.filled(
|
|
||||||
icon: FaIcon(icon),
|
|
||||||
iconSize: iconSize ?? 18,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: BoxConstraints(),
|
|
||||||
style: style,
|
|
||||||
onPressed: onPressed,
|
|
||||||
color: iconColor,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return IconButton(
|
|
||||||
icon: FaIcon(icon),
|
|
||||||
iconSize: iconSize ?? 18,
|
|
||||||
padding: EdgeInsets.all(4),
|
|
||||||
constraints: BoxConstraints(),
|
|
||||||
style: style,
|
|
||||||
onPressed: onPressed,
|
|
||||||
color: iconColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
|
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
|
||||||
@ -124,16 +107,13 @@ class UI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget metaCard({required BuildContext context, required IconData icon, required String title, required List<String> values, void Function()? mainAction, List<(IconData, Color?, void Function())>? iconActions}) {
|
static Widget metaCard({required BuildContext context, required IconData icon, required String title, required List<String> values, void Function()? mainAction, List<(IconData, void Function())>? iconActions}) {
|
||||||
final container = UI.box(
|
final container = UI.box(
|
||||||
context: context,
|
context: context,
|
||||||
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
FaIcon(icon, size: 18),
|
||||||
constraints: new BoxConstraints(minWidth: 18.0),
|
|
||||||
child: Center(child: FaIcon(icon, size: 18)),
|
|
||||||
),
|
|
||||||
SizedBox(width: 16),
|
SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -148,7 +128,7 @@ class UI {
|
|||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
for (final iconAction in iconActions) ...[
|
for (final iconAction in iconActions) ...[
|
||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
IconButton(icon: FaIcon(iconAction.$1), color: iconAction.$2, onPressed: iconAction.$3),
|
IconButton(icon: FaIcon(iconAction.$1), onPressed: iconAction.$2),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@ name: simplecloudnotifier
|
|||||||
description: "Receive push messages"
|
description: "Receive push messages"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 2.0.0+474
|
version: 2.0.0+100
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.6 <4.0.0'
|
sdk: '>=3.2.6 <4.0.0'
|
||||||
@ -11,7 +11,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_launcher_icons: ^0.14.3
|
flutter_launcher_icons: "^0.13.1"
|
||||||
|
|
||||||
font_awesome_flutter: '>= 4.7.0'
|
font_awesome_flutter: '>= 4.7.0'
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
@ -21,25 +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.20.2
|
intl: ^0.19.0
|
||||||
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: ^3.13.0
|
firebase_core: ^2.32.0
|
||||||
firebase_messaging: ^15.2.5
|
firebase_messaging: ^14.9.4
|
||||||
device_info_plus: ^11.3.0
|
device_info_plus: ^10.1.0
|
||||||
toastification: ^3.0.1
|
toastification: ^2.0.0
|
||||||
uuid: ^4.4.0
|
uuid: ^4.4.0
|
||||||
share_plus: ^10.1.4
|
share_plus: ^9.0.0
|
||||||
flutter_local_notifications: ^17.2.3
|
flutter_local_notifications: ^17.1.2
|
||||||
|
|
||||||
|
|
||||||
path: any
|
path: any
|
||||||
mobile_scanner: ^6.0.1
|
mobile_scanner: ^6.0.1
|
||||||
settings_ui: ^2.0.2
|
|
||||||
git_stamp: ^5.10.0
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
font_awesome_flutter:
|
font_awesome_flutter:
|
||||||
path: deps/font_awesome_flutter
|
path: deps/font_awesome_flutter
|
||||||
@ -49,7 +47,7 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^4.0.0
|
||||||
hive_generator: ^2.0.1
|
hive_generator: ^2.0.1
|
||||||
build_runner: ^2.1.4
|
build_runner: ^2.1.4
|
||||||
|
|
||||||
|
2
scnserver/.idea/sqldialects.xml
generated
2
scnserver/.idea/sqldialects.xml
generated
@ -1,9 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="SqlDialectMappings">
|
<component name="SqlDialectMappings">
|
||||||
<file url="file://$PROJECT_DIR$/db/schema/primary_10.ddl" dialect="SQLite" />
|
|
||||||
<file url="file://$PROJECT_DIR$/db/schema/primary_3.ddl" dialect="SQLite" />
|
<file url="file://$PROJECT_DIR$/db/schema/primary_3.ddl" dialect="SQLite" />
|
||||||
<file url="file://$PROJECT_DIR$/db/schema/primary_migration_9_10.sql" dialect="SQLite" />
|
|
||||||
<file url="PROJECT" dialect="SQLite" />
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
</component>
|
</component>
|
||||||
<component name="SqlResolveMappings">
|
<component name="SqlResolveMappings">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user