1 Commits

Author SHA1 Message Date
be04082d25 Reduce MaxOpenConns to 1 (less SQLITE_BUSY) ?!? 2024-10-03 14:37:48 +02:00
52 changed files with 666 additions and 1492 deletions

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -30,7 +30,6 @@ android {
targetCompatibility 1.8
sourceCompatibility 1.8
}
namespace 'com.blackforestbytes.simplecloudnotifier'
}
dependencies {

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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.READ_EXTERNAL_STORAGE" />

View File

@@ -7,8 +7,8 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.1'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.gms:google-services:4.3.4'
}
}

View File

@@ -14,6 +14,3 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

2
flutter/.gitignore vendored
View File

@@ -5,8 +5,6 @@
firepit-log.txt
flutter_jank_*
_releases/*
#######################################################################################################################

View File

@@ -1,39 +1,14 @@
# Setup
#
# flutter config --jdk-dir "/usr/lib/jvm/default-runtime/bin"
# sudo archlinux-java set java-17-openjdk
#
run:
flutter pub run build_runner build
flutter run
# runs app locally (linux)
run-linux:
dart run build_runner build
_JAVA_OPTIONS="" flutter run -d linux
# runs app locally (web | not really supported)
run-linux:
dart run build_runner build
_JAVA_OPTIONS="" flutter run -d web
# runs on android device (must have network adb enabled teh correct IP)
run-android:
ping -c1 10.10.10.177
adb connect 10.10.10.177:5555
flutter pub run build_runner build
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
install-release:
# Install on Pixel 7a
flutter build apk --release
flutter run --release -d 35221JEHN07157
build-release:
flutter build apk --release
flutter build appbundle --release
flutter build linux --release
flutter run -d 10.10.10.177:5555
test:
dart analyze
@@ -44,23 +19,9 @@ fix:
gen:
flutter pub run build_runner build
# run `make run` in another terminal (or another variant of flutter run)
autoreload:
@
@# run `make run` in another terminal (or another variant of flutter run)
@_utils/autoreload.sh
icons:
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
clean:
cd android && ./gradlew clean
flutter clean
# upgrade all packages (add --major-versions even updates across new major versions)
# https://docs.flutter.dev/release/upgrade
# upgrading flutter can be done via `flutter upgrade`: https://docs.flutter.dev/release/upgrade
# android/gradle updates should be done via androidStudio: https://docs.flutter.dev/release/breaking-changes/android-java-gradle-migration-guide
upgrade:
flutter upgrade
flutter pub upgrade
flutter doctor

View File

@@ -25,15 +25,6 @@
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?
- [ ] fix time format (in message-list, in card, top right) - midnight is shown as "24:05" instead of "00:05" - thats weird
- [ ] Add scrollbar
-> https://api.flutter.dev/flutter/material/Scrollbar-class.html
- [ ] you cant unsubscribe from foreign channel without completely loosing subscription.
perhaps subscriptions should have two cofirmed bool (both must be true to receive messages): confirmed-owner && confirmed-subscriber
Then the subscriber can unconfirm his half - without loosing the owner confirmation
-----
# TODO iOS specific

View File

@@ -11,7 +11,3 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
build/
app/.cxx/

View File

@@ -34,16 +34,16 @@ if (keystorePropertiesFile.exists()) {
android {
namespace "com.blackforestbytes.simplecloudnotifier"
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 {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '17'
jvmTarget = '1.8'
}
sourceSets {

View File

@@ -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 {
repositories {
google()

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View File

@@ -23,8 +23,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
id "com.android.application" version "7.3.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration

View File

@@ -45,10 +45,5 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photos access to get QR code from photo library</string>
</dict>
</plist>

View File

@@ -5,7 +5,6 @@ import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/api_error.dart';
import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
@@ -102,23 +101,21 @@ class APIClient {
}
if (responseStatusCode != 200) {
APIError apierr;
try {
apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
final apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", 'Request "${name}" failed');
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
} catch (exc, trace) {
ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace);
}
RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
Toaster.error("Error", 'Request "${name}" failed');
throw Exception('API request failed with status code ${responseStatusCode}');
}
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", apierr.message);
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
}
try {
final data = jsonDecode(responseBody);
@@ -162,20 +159,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<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
return await _request(
name: 'addClient',
@@ -298,20 +281,6 @@ class APIClient {
);
}
static Future<(String, List<SCNMessage>)> getChannelMessageList(TokenSource auth, String cid, String pageToken, {int? pageSize}) async {
return await _request(
name: 'getChannelMessageList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels/${cid}/messages',
query: {
'next_page_token': [pageToken],
if (pageSize != null) 'page_size': [pageSize.toString()],
},
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
authToken: auth.getToken(),
);
}
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
return await _request(
name: 'getSubscriptionList',
@@ -400,62 +369,7 @@ class APIClient {
);
}
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
return await _request(
name: 'getSenderNameList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/sender-names',
fn: (json) => SenderNameStatistics.fromJsonArray(json['sender_names'] as List<dynamic>),
authToken: auth.getToken(),
);
}
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID) async {
return await _request(
name: 'subscribeToChannelbyID',
method: 'POST',
relURL: 'users/${auth.getUserID()}/subscriptions',
jsonBody: {
'channel_id': channelID,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> deleteSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'deleteSubscription',
method: 'DELETE',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> confirmSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'confirmSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'confirmed': true,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> unconfirmSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'unconfirmSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'confirmed': false,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
static Future<List<String>> getSenderNameList(AppAuth userAcc) {
return Future.value(['TODO']); //TODO
}
}

View File

@@ -1,7 +1,7 @@
class APIException implements Exception {
final int httpStatus;
final int error;
final int errHighlight;
final String errHighlight;
final String message;
APIException(this.httpStatus, this.error, this.errHighlight, this.message);

View File

@@ -25,7 +25,7 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
final userAcc = Provider.of<AppAuth>(context, listen: false);
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);
return senders;
}());

View File

@@ -1,4 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
class FilterModalTime extends StatefulWidget {
@override

View File

@@ -59,7 +59,6 @@ void main() async {
Hive.deleteBoxFromDisk('scn-logs');
await Hive.openBox<SCNLog>('scn-logs');
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-logs', {'error': exc.toString(), 'trace': trace});
}
print('[INIT] Load Hive<scn-requests>...');
@@ -70,7 +69,6 @@ void main() async {
Hive.deleteBoxFromDisk('scn-requests');
await Hive.openBox<SCNRequest>('scn-requests');
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-requests', {'error': exc.toString(), 'trace': trace});
}
print('[INIT] Load Hive<scn-message-cache>...');
@@ -81,7 +79,6 @@ void main() async {
Hive.deleteBoxFromDisk('scn-message-cache');
await Hive.openBox<SCNMessage>('scn-message-cache');
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-message-cache', {'error': exc.toString(), 'trace': trace});
}
print('[INIT] Load Hive<scn-channel-cache>...');
@@ -92,7 +89,6 @@ void main() async {
Hive.deleteBoxFromDisk('scn-channel-cache');
await Hive.openBox<Channel>('scn-channel-cache');
ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-channel-cache', {'error': exc.toString(), 'trace': trace});
}
print('[INIT] Load Hive<scn-fb-messages>...');
@@ -103,7 +99,6 @@ void main() async {
Hive.deleteBoxFromDisk('scn-fb-messages');
await Hive.openBox<FBMessage>('scn-fb-messages');
ApplicationLog.error('Failed to open Hive-Box: scn-fb-messages' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
}
print('[INIT] Load AppAuth...');
@@ -117,13 +112,11 @@ void main() async {
await appAuth.loadUser();
} catch (exc, trace) {
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace});
}
try {
await appAuth.loadClient();
} catch (exc, trace) {
ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
ApplicationLog.writeRawFailure('Failed to load user (background load on startup)', {'error': exc.toString(), 'trace': trace});
}
}();
}
@@ -226,7 +219,7 @@ class SCNApp extends StatelessWidget {
return ToastificationWrapper(
config: ToastificationConfig(
itemWidth: 440,
marginBuilder: (context, alignment) => EdgeInsets.symmetric(vertical: 64),
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
animationDuration: Duration(milliseconds: 200),
),
child: Consumer<AppTheme>(
@@ -328,7 +321,6 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
await Hive.openBox<SCNMessage>('scn-message-cache');
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
return;
@@ -349,7 +341,6 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
} catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to decode received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
return;
@@ -358,7 +349,6 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
try {
FBMessageLog.insert(message);
} catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to persist received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
return;
@@ -369,7 +359,6 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
SCNDataCache().addToMessageCache([msg]);
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
} catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to query+persist message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground});
ApplicationLog.error('Failed to query+persist message: ' + exc.toString(), trace: trace);
return;
}
@@ -393,7 +382,7 @@ void _handleNotificationClickAction(String? payload, Duration delay) {
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
final messageID = parts[1];
() async {
await Future.delayed(delay, () {});
await Future.delayed(delay);
SchedulerBinding.instance.addPostFrameCallback((_) {
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
@@ -403,7 +392,7 @@ void _handleNotificationClickAction(String? payload, Duration delay) {
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
final channelID = parts[1];
() async {
await Future.delayed(delay, () {});
await Future.delayed(delay);
SchedulerBinding.instance.addPostFrameCallback((_) {
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');

View File

@@ -1,7 +1,7 @@
class APIError {
final bool success;
final int error;
final int errhighlight;
final String errhighlight;
final String message;
static final MISSING_UID = 1101;
@@ -67,7 +67,7 @@ class APIError {
return APIError(
success: json['success'] as bool,
error: (json['error'] as num).toInt(),
errhighlight: (json['errhighlight'] as num).toInt(),
errhighlight: json['errhighlight'] as String,
message: json['message'] as String,
);
}

View File

@@ -1,75 +0,0 @@
import 'package:simplecloudnotifier/models/channel.dart';
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
abstract class ScanResult {
ScanResultMode get mode;
static ScanResult? parse(String v) {
var lines = v.split('\n');
if (lines.length == 1 && lines[0].startsWith('https://simplecloudnotifier.de?')) {
final v = Uri.tryParse(lines[0]);
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key']);
}
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null);
}
}
if (lines.length == 6 && lines[0] == '@scn.channel.subscribe' && lines[1] == 'v1') {
return ScanResultChannelSubscribe(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4], subscribeKey: lines[5]);
}
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
if (lines.length != 4) return null;
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
}
return null;
}
static String createChannelQR(Channel channel) {
return '@scn.channel' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID;
}
static String createChannelSubscribeQR(Channel channel, String subscribeKey) {
return '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID + '\n' + subscribeKey;
}
}
class ScanResultMessageSend extends ScanResult {
final String userID;
final String? userKey;
ScanResultMessageSend({required this.userID, required this.userKey});
@override
ScanResultMode get mode => ScanResultMode.MessageSend;
}
class ScanResultChannel extends ScanResult {
final String channelDisplayName;
final String ownerUserID;
final String channelID;
ScanResultChannel({required this.channelDisplayName, required this.ownerUserID, required this.channelID});
@override
ScanResultMode get mode => ScanResultMode.Channel;
}
class ScanResultChannelSubscribe extends ScanResult {
final String channelDisplayName;
final String ownerUserID;
final String channelID;
final String subscribeKey;
ScanResultChannelSubscribe({required this.channelDisplayName, required this.ownerUserID, required this.channelID, required this.subscribeKey});
@override
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
}

View File

@@ -1,26 +0,0 @@
class SenderNameStatistics {
final String name;
final String lastTimestamp;
final String firstTimestamp;
final int count;
const SenderNameStatistics({
required this.name,
required this.lastTimestamp,
required this.firstTimestamp,
required this.count,
});
factory SenderNameStatistics.fromJson(Map<String, dynamic> json) {
return SenderNameStatistics(
name: json['name'] as String,
lastTimestamp: json['last_timestamp'] as String,
firstTimestamp: json['first_timestamp'] as String,
count: json['count'] as int,
);
}
static List<SenderNameStatistics> fromJsonArray(List<dynamic> jsonArr) {
return jsonArr.map<SenderNameStatistics>((e) => SenderNameStatistics.fromJson(e as Map<String, dynamic>)).toList();
}
}

View File

@@ -7,13 +7,11 @@ import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/account/login.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
@@ -34,7 +32,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
late ImmediateFuture<int>? futureKeyCount;
late ImmediateFuture<int>? futureChannelAllCount;
late ImmediateFuture<int>? futureChannelSubscribedCount;
late ImmediateFuture<int>? futureSenderNamesCount;
late ImmediateFuture<User>? futureUser;
late AppAuth userAcc;
@@ -90,7 +87,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
futureKeyCount = null;
futureChannelAllCount = null;
futureChannelSubscribedCount = null;
futureSenderNamesCount = null;
if (userAcc.isAuth()) {
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
@@ -123,12 +119,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
return keys.length;
}());
futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in');
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
return senders.length;
}());
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
}
}
@@ -147,7 +137,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
final subs = await APIClient.getSubscriptionList(userAcc);
final clients = await APIClient.getClientList(userAcc);
final keys = await APIClient.getKeyTokenList(userAcc);
final senderNames = await APIClient.getSenderNameList(userAcc);
final user = await userAcc.loadUser(force: true);
setState(() {
@@ -156,7 +145,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
futureClientCount = ImmediateFuture.ofValue(clients.length);
futureKeyCount = ImmediateFuture.ofValue(keys.length);
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
futureUser = ImmediateFuture.ofValue(user);
});
} catch (exc, trace) {
@@ -360,15 +348,13 @@ class _AccountRootPageState extends State<AccountRootPage> {
mainAxisAlignment: MainAxisAlignment.start,
children: [
UI.buttonIconOnly(
onPressed: _changeUsername,
onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.pen,
),
const SizedBox(height: 4),
if (!user.isPro)
UI.buttonIconOnly(
onPressed: () {
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
},
onPressed: () {/*TODO*/},
icon: FontAwesomeIcons.cartCircleArrowUp,
),
],
@@ -382,10 +368,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {
Navi.push(context, () => ChannelListExtendedPage());
}),
_buildNumberCard(context, 'Sender', futureSenderNamesCount, () {/*TODO*/}),
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
@@ -505,31 +488,4 @@ class _AccountRootPageState extends State<AccountRootPage> {
void _deleteAccount() async {
//TODO
}
void _changeUsername() async {
final acc = AppAuth();
if (!acc.isAuth()) return;
var newusername = await UIDialogs.showTextInput(context, 'Change your public username', 'Enter new username');
if (newusername == null) return;
newusername = newusername.trim();
if (newusername == '') {
Toaster.error("Error", 'Username cannot be empty');
return;
}
try {
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
setState(() {
futureUser = ImmediateFuture.ofValue(user);
});
Toaster.success("Success", 'Username changed');
_backgroundRefresh();
} catch (exc, trace) {
Toaster.error("Error", 'Failed to update username');
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
}
}
}

View File

@@ -140,14 +140,12 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
return;
}
if (stokv != "") {
final toks = await APIClient.getKeyTokenByToken(uid, stokv);
if (!toks.allChannels || toks.permissions != 'CS') {
Toaster.error("Error", 'Send token does not have required permissions');
return;
}
}
final user = await APIClient.getUser(DirectTokenSource(uid, atokv), uid);

View File

@@ -4,7 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_scanner.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
@@ -154,13 +154,8 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Messages,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
onPressed: () {
Navi.push(context, () => ChannelViewPage(channelID: item.channel.channelID, preloadedData: (item.channel, item.subscription), needsReload: _enqueueReload));
},
),
),
@@ -169,7 +164,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
floatingActionButton: FloatingActionButton(
heroTag: 'fab_channel_list_qr',
onPressed: () {
Navi.push(context, () => ChannelScannerPage());
//TODO scan qr code to subscribe channel
},
child: const Icon(FontAwesomeIcons.qrcode),
),

View File

@@ -1,151 +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/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class ChannelListExtendedPage extends StatefulWidget {
const ChannelListExtendedPage({super.key});
@override
State<ChannelListExtendedPage> createState() => _ChannelListExtendedPageState();
}
class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with RouteAware {
final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
bool _reloadEnqueued = false;
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_fetchPage);
_pagingController.refresh();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void dispose() {
ApplicationLog.debug('ChannelRootPage::dispose');
_pagingController.dispose();
Navi.modalRouteObserver.unsubscribe(this);
super.dispose();
}
@override
void didPopNext() {
if (_reloadEnqueued) {
ApplicationLog.debug('[ChannelList::RouteObserver] --> didPopNext (will background-refresh) (_reloadEnqueued == true)');
() async {
_reloadEnqueued = false;
await Future.delayed(const Duration(milliseconds: 500), () {}); // prevents flutter bug where the whole process crashes ?!?
await _backgroundRefresh();
}();
}
}
Future<void> _fetchPage(int pageKey) async {
final acc = Provider.of<AppAuth>(context, listen: false);
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
if (!acc.isAuth()) {
_pagingController.error = 'Not logged in';
return;
}
try {
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
} catch (exc, trace) {
_pagingController.error = exc.toString();
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
}
}
Future<void> _backgroundRefresh() async {
final acc = Provider.of<AppAuth>(context, listen: false);
ApplicationLog.debug('Start background refresh of channel list');
if (!acc.isAuth()) {
_pagingController.error = 'Not logged in';
return;
}
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
AppBarState().setLoadingIndeterminate(true);
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
setState(() {
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
});
} catch (exc, trace) {
setState(() {
_pagingController.error = exc.toString();
});
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
} finally {
AppBarState().setLoadingIndeterminate(false);
}
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: "Channels",
showSearch: false,
showShare: false,
child: Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<int, ChannelWithSubscription>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
itemBuilder: (context, item, index) => ChannelListItem(
channel: item.channel,
subscription: item.subscription,
mode: ChannelListItemMode.Extended,
onChannelListReloadTrigger: _enqueueReload,
onSubscriptionChanged: (channelID, subscription) {
setState(() {
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
});
},
),
),
),
),
),
);
}
void _enqueueReload() {
_reloadEnqueued = true;
}
}

View File

@@ -1,4 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
@@ -7,35 +9,24 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
enum ChannelListItemMode {
Messages,
Extended,
}
class ChannelListItem extends StatefulWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
const ChannelListItem({
required this.channel,
required this.onChannelListReloadTrigger,
required this.onSubscriptionChanged,
required this.onPressed,
required this.subscription,
required this.mode,
super.key,
});
final Channel channel;
final Subscription? subscription;
final void Function() onChannelListReloadTrigger;
final ChannelListItemMode mode;
final void Function(String, Subscription?) onSubscriptionChanged;
final Null Function() onPressed;
@override
State<ChannelListItem> createState() => _ChannelListItemState();
@@ -50,11 +41,11 @@ class _ChannelListItemState extends State<ChannelListItem> {
final acc = Provider.of<AppAuth>(context, listen: false);
if (acc.isAuth() && widget.mode == ChannelListItemMode.Messages) {
if (acc.isAuth()) {
lastMessage = SCNDataCache().getMessagesSorted().where((p) => p.channelID == widget.channel.channelID).firstOrNull;
() async {
final (_, channelMessages) = await APIClient.getChannelMessageList(acc, widget.channel.channelID, '@start', pageSize: 1);
final (_, channelMessages) = await APIClient.getMessageList(acc, '@start', pageSize: 1, filter: MessageFilter(channelIDs: [widget.channel.channelID]));
setState(() {
lastMessage = channelMessages.firstOrNull;
});
@@ -64,18 +55,13 @@ class _ChannelListItemState extends State<ChannelListItem> {
@override
Widget build(BuildContext context) {
//TODO subscription status
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: () {
if (widget.mode == ChannelListItemMode.Messages) {
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
} else {
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
}
},
onTap: widget.onPressed,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
@@ -104,8 +90,13 @@ class _ChannelListItemState extends State<ChannelListItem> {
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(child: (widget.mode == ChannelListItemMode.Messages) ? Text(_preformatTitle(lastMessage), style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))) : _buildSubscriptionStateText(context)),
(widget.mode == ChannelListItemMode.Messages) ? Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)) : Text("", style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
Expanded(
child: Text(
_preformatTitle(lastMessage),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
),
),
Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
],
),
],
@@ -114,15 +105,11 @@ class _ChannelListItemState extends State<ChannelListItem> {
SizedBox(width: 4),
GestureDetector(
onTap: () {
if (widget.mode == ChannelListItemMode.Messages) {
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
} else {
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
}
Navi.push(context, () => ChannelMessageViewPage(channel: this.widget.channel));
},
child: Padding(
padding: const EdgeInsets.all(8),
child: (widget.mode == ChannelListItemMode.Messages) ? Icon(FontAwesomeIcons.solidSquareInfo, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24) : Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
),
),
],
@@ -139,73 +126,13 @@ class _ChannelListItemState extends State<ChannelListItem> {
Widget _buildIcon(BuildContext context) {
if (widget.subscription == null) {
Widget result = Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
result = GestureDetector(onTap: () => _subscribe(), child: result);
return result;
return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
return result;
return Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
} 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;
return Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
} 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;
}
}
Widget _buildSubscriptionStateText(BuildContext context) {
if (widget.subscription == null) {
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else if (widget.subscription!.confirmed) {
return Text("subscripted (foreign channe)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
} else {
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
}
}
void _subscribe() async {
final acc = AppAuth();
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID()) {
try {
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channel.channelID);
widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged(widget.channel.channelID, sub);
if (sub.confirmed) {
Toaster.success("Success", 'Subscribed to channel');
} else {
Toaster.success("Success", 'Requested widget.subscription to channel');
}
} catch (exc, trace) {
Toaster.error("Error", 'Failed to subscribe to channel');
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
}
}
}
void _unsubscribe(Subscription sub) async {
final acc = AppAuth();
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID() && widget.subscription != null) {
try {
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged.call(widget.channel.channelID, null);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
}
}
}

View File

@@ -1,117 +0,0 @@
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
);
}
}

View File

@@ -55,7 +55,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
}
try {
final (npt, newItems) = await APIClient.getChannelMessageList(acc, this.widget.channel.channelID, thisPageToken, pageSize: cfg.messagePageSize);
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: MessageFilter(channelIDs: [this.widget.channel.channelID]));
SCNDataCache().addToMessageCache(newItems); // no await

View File

@@ -5,7 +5,6 @@ import 'package:share_plus/share_plus.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/scan_result.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
@@ -64,15 +63,16 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
@override
void initState() {
_initStateAsync(true);
_initStateAsync();
super.initState();
}
Future<void> _initStateAsync(bool usePreload) async {
@override
void _initStateAsync() async {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (widget.preloadedData != null && usePreload) {
if (widget.preloadedData != null) {
channelPreview = widget.preloadedData!.$1.toPreview();
channel = widget.preloadedData!.$1;
subscription = widget.preloadedData!.$2;
@@ -232,7 +232,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (foreign)',
title: 'Subscription (own)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
),
@@ -296,8 +296,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
return FutureBuilder(
future: _futureSubscribeKey.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
final text = (snapshot.data == null) ? ScanResult.createChannelQR(channel!) : ScanResult.createChannelSubscribeQR(channel!, snapshot.data!);
if (snapshot.hasData && snapshot.data != null) {
var text = 'TODO' + '\n' + channel!.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
return GestureDetector(
onTap: () {
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
@@ -306,7 +306,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
child: QrImageView(
data: text,
version: QrVersions.auto,
size: 265.0,
size: 300.0,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).textTheme.bodyLarge?.color,
@@ -318,6 +318,12 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
),
),
);
} else if (snapshot.hasData && snapshot.data == null) {
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: Icon(FontAwesomeIcons.solidSnake, size: 64)),
);
} else {
return const SizedBox(
width: 300.0,
@@ -441,6 +447,14 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
}
}
void _subscribe() {
//TODO
}
void _unsubscribe() {
//TODO
}
void _showEditDisplayName() {
setState(() {
_ctrlDisplayName.text = _displayNameOverride ?? channelPreview?.displayName ?? '';
@@ -505,90 +519,16 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
}
}
void _subscribe() async {
final acc = AppAuth();
try {
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channelID);
widget.needsReload?.call();
await _initStateAsync(false);
if (sub.confirmed) {
Toaster.success("Success", 'Subscribed to channel');
} else {
Toaster.success("Success", 'Requested subscription to channel');
}
} 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) {
//TODO
}
void _unsubscribe() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to unsubscribe from channel');
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
}
void _confirmForeignSubscription(Subscription sub) {
//TODO
}
void _cancelForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.unconfirmSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully revoked');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to revoke subscription');
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
}
}
void _confirmForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.confirmSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
}
}
void _denyForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.deleteSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription request succesfully denied');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to deny subscription');
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
}
void _denyForeignSubscription(Subscription sub) {
//TODO
}
String _formatSubscriptionStatus(Subscription? subscription) {

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/utils/notifier.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';

View File

@@ -55,11 +55,11 @@ class _DebugColorsPageState extends State<DebugColorsPage> {
buildCol("colorScheme.surface", Theme.of(context).colorScheme.surface),
buildCol("colorScheme.onSurface", Theme.of(context).colorScheme.onSurface),
buildCol("colorScheme.surfaceTint", Theme.of(context).colorScheme.surfaceTint),
buildCol("colorScheme.surfaceVariant", Theme.of(context).colorScheme.surfaceContainerHighest),
buildCol("colorScheme.surfaceVariant", Theme.of(context).colorScheme.surfaceVariant),
buildCol("colorScheme.inverseSurface", Theme.of(context).colorScheme.inverseSurface),
buildCol("colorScheme.onInverseSurface", Theme.of(context).colorScheme.onInverseSurface),
buildCol("colorScheme.background", Theme.of(context).colorScheme.surface),
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onSurface),
buildCol("colorScheme.background", Theme.of(context).colorScheme.background),
buildCol("colorScheme.onBackground", Theme.of(context).colorScheme.onBackground),
buildCol("colorScheme.error", Theme.of(context).colorScheme.error),
buildCol("colorScheme.onError", Theme.of(context).colorScheme.onError),
buildCol("colorScheme.errorContainer", Theme.of(context).colorScheme.errorContainer),
@@ -98,7 +98,7 @@ class _DebugColorsPageState extends State<DebugColorsPage> {
buildCol("badgeTheme.backgroundColor", Theme.of(context).badgeTheme.backgroundColor),
buildCol("bannerTheme.backgroundColor", Theme.of(context).bannerTheme.backgroundColor),
buildCol("bottomAppBarTheme.color", Theme.of(context).bottomAppBarTheme.color),
buildCol("buttonTheme.colorScheme.background", Theme.of(context).buttonTheme.colorScheme?.surface),
buildCol("buttonTheme.colorScheme.background", Theme.of(context).buttonTheme.colorScheme?.background),
buildCol("buttonTheme.colorScheme.primary", Theme.of(context).buttonTheme.colorScheme?.primary),
buildCol("buttonTheme.colorScheme.secondary", Theme.of(context).buttonTheme.colorScheme?.secondary),
buildCol("cardTheme.color", Theme.of(context).cardTheme.color),

View File

@@ -60,7 +60,7 @@ class _DebugMainPageState extends State<DebugMainPage> {
ButtonSegment<DebugMainPageSubPage>(value: DebugMainPageSubPage.logs, icon: Icon(FontAwesomeIcons.solidFileLines, size: 14)),
],
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
padding: MaterialStateProperty.all<EdgeInsets>(EdgeInsets.fromLTRB(0, 0, 0, 0)),
visualDensity: VisualDensity(horizontal: -3, vertical: -3),
),
selected: <DebugMainPageSubPage>{_subPage},

View File

@@ -1,20 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogs.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/fb_message.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/interfaces.dart';
import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:path/path.dart' as path;
class DebugPersistencePage extends StatefulWidget {
@override
@@ -44,7 +39,6 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
_buildHiveCard(context, () => Hive.box<SCNMessage>('scn-message-cache'), 'scn-message-cache'),
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
_buildFailureLogCard(context, Globals().rawFailureLogsDir),
],
),
);
@@ -91,25 +85,4 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
),
);
}
Widget _buildFailureLogCard(BuildContext context, Directory dir) {
return Card.outlined(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
Navi.push(context, () => DebugFailureLogsPage(dir: dir.path));
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(width: 30, child: Text('')),
Expanded(child: Text('Failure [/${path.basename(dir.path)}/]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
SizedBox(width: 40, child: Text("${dir.listSync().length}", textAlign: TextAlign.end)),
],
),
),
),
);
}
}

View File

@@ -1,63 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
class DebugFailureLogFilePage extends StatefulWidget {
final String path;
DebugFailureLogFilePage({required this.path}) {}
@override
State<DebugFailureLogFilePage> createState() => _DebugFailureLogFilePageState();
}
class _DebugFailureLogFilePageState extends State<DebugFailureLogFilePage> {
ImmediateFuture<String>? _futureContent;
@override
void initState() {
super.initState();
_futureContent = ImmediateFuture.ofFuture(new File(this.widget.path).readAsString());
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: 'FailureLog',
showSearch: false,
child: () {
if (_futureContent == null) {
return Center(child: CircularProgressIndicator());
}
return FutureBuilder(
future: _futureContent!.future,
builder: ((context, snapshot) {
if (_futureContent?.value != null) {
return _buildContent(context, _futureContent!.value!);
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
return Text('Error: ${snapshot.error}'); //TODO better error display
} else if (snapshot.connectionState == ConnectionState.done) {
return _buildContent(context, snapshot.data!);
} else {
return Center(child: CircularProgressIndicator());
}
}),
);
}(),
);
}
Widget _buildContent(BuildContext context, String value) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(value, style: TextStyle(fontFamily: "monospace")),
),
);
}
}

View File

@@ -1,86 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_failurelogfile.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:path/path.dart' as path;
import 'package:simplecloudnotifier/utils/toaster.dart';
class DebugFailureLogsPage extends StatefulWidget {
final String dir;
DebugFailureLogsPage({required this.dir});
@override
State<DebugFailureLogsPage> createState() => _DebugFailureLogsPageState();
}
class _DebugFailureLogsPageState extends State<DebugFailureLogsPage> {
List<String> files = [];
_DebugFailureLogsPageState() {
files = _listFilesInRawLogFolder();
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: 'F-Logs',
showSearch: false,
child: ListView.separated(
itemCount: files.length,
itemBuilder: (context, listIndex) {
return GestureDetector(
onTap: () {
Navi.push(context, () => DebugFailureLogFilePage(path: files[listIndex]));
},
child: Container(
padding: EdgeInsets.fromLTRB(8, 0, 8, 0),
child: Row(
children: [
Expanded(child: Text(path.basename(files[listIndex]), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))),
IconButton(
icon: const Icon(FontAwesomeIcons.trash),
tooltip: 'Delete',
iconSize: 16,
color: Colors.red,
onPressed: () => _deleteFile(context, files[listIndex]),
)
],
),
),
);
},
separatorBuilder: (context, index) => Divider(),
),
);
}
List<String> _listFilesInRawLogFolder() {
final fse = Globals().rawFailureLogsDir.listSync();
ApplicationLog.debug("Found ${fse.length} files in raw log folder '${Globals().rawFailureLogsDir.path}'");
var paths = fse.where((element) => element is File).map((e) => e.path).toList();
paths.sort((a, b) => -1 * a.compareTo(b));
return paths;
}
void _deleteFile(BuildContext context, String fil) {
final file = File(fil);
file.deleteSync();
setState(() {
files = _listFilesInRawLogFolder();
});
Toaster.info("Okay", "File deleted");
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/ui.dart';
class DebugRequestViewPage extends StatefulWidget {
class DebugRequestViewPage extends StatelessWidget {
final SCNRequest request;
DebugRequestViewPage({required this.request});
@override
State<DebugRequestViewPage> createState() => _DebugRequestViewPageState();
}
class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
Set<String> _monospaceMode = new Set();
Set<String> _prettyJson = new Set();
@override
Widget build(BuildContext context) {
return SCNScaffold(
@@ -33,22 +23,22 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
...buildRow(context, "name", "Name", widget.request.name),
...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()),
...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()),
...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()),
...buildRow(context, "Name", request.name),
...buildRow(context, "Timestamp (Start)", request.timestampStart.toString()),
...buildRow(context, "Timestamp (End)", request.timestampEnd.toString()),
...buildRow(context, "Duration", request.timestampEnd.difference(request.timestampStart).toString()),
Divider(),
...buildRow(context, "method", "Method", widget.request.method),
...buildRow(context, "url", "URL", widget.request.url, mono: true),
if (widget.request.requestHeaders.isNotEmpty) ...buildRow(context, "request_headers", "Request->Headers", widget.request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true),
if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true),
...buildRow(context, "Method", request.method),
...buildRow(context, "URL", request.url),
if (request.requestHeaders.isNotEmpty) ...buildRow(context, "Request->Headers", request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
if (request.requestBody != '') ...buildRow(context, "Request->Body", request.requestBody),
Divider(),
if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()),
if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true),
if (widget.request.responseHeaders.isNotEmpty) ...buildRow(context, "response_headers", "Reponse->Headers", widget.request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true, json: true),
if (request.responseStatusCode != 0) ...buildRow(context, "Response->Statuscode", request.responseStatusCode.toString()),
if (request.responseBody != '') ...buildRow(context, "Reponse->Body", request.responseBody),
if (request.responseHeaders.isNotEmpty) ...buildRow(context, "Reponse->Headers", request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n')),
Divider(),
if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true),
if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true),
if (request.error != '') ...buildRow(context, "Error", request.error),
if (request.stackTrace != '') ...buildRow(context, "Stacktrace", request.stackTrace),
],
),
),
@@ -56,19 +46,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
);
}
List<Widget> buildRow(BuildContext context, String key, String title, String value, {bool? json, bool? mono}) {
var isMono = _monospaceMode.contains(key);
var isJson = _prettyJson.contains(key);
if (isJson) {
try {
var jsonValue = jsonDecode(value);
value = JsonEncoder.withIndent(' ').convert(jsonValue);
} catch (e) {
value = ('Error parsing JSON: $e');
}
}
List<Widget> buildRow(BuildContext context, String title, String value) {
return [
Padding(
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 8.0),
@@ -86,46 +64,19 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
},
icon: FontAwesomeIcons.copy,
),
if (mono == true)
UI.buttonIconOnly(
icon: isMono ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
onPressed: () {
setState(() {
_monospaceMode.contains(key) ? _monospaceMode.remove(key) : _monospaceMode.add(key);
});
},
),
if (json == true)
UI.buttonIconOnly(
icon: isJson ? FontAwesomeIcons.bracketsRound : FontAwesomeIcons.bracketsCurly,
onPressed: () {
setState(() {
_prettyJson.contains(key) ? _prettyJson.remove(key) : _prettyJson.add(key);
});
},
),
],
),
),
Card.filled(
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
color: widget.request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
color: request.type == 'SUCCESS' ? null : Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
child: (isMono || isJson)
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SelectableText(
value,
minLines: 1,
maxLines: 10,
),
)
: SelectableText(
value,
minLines: 1,
maxLines: 10,
),
),
),
];

View File

@@ -72,9 +72,6 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
_channels = SCNDataCache().getChannelMap();
//TODO this is not 100% correct - the message-cache contains (which is right!) all messages, even from unsubscribed channels
//TODO what we should do is save another list in SCNDataCache, with the result of the last getMessageList call (page-1) and use that
//TODO this way we only get 1 page of data from cache, but its a weird behaviour anway that we loose data once _backgroundRefresh is finished
_pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
_backgroundRefresh(true);

View File

@@ -1,11 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/interfaces.dart';
import 'package:xid/xid.dart';
import 'package:path/path.dart' as path;
part 'application_log.g.dart';
@@ -81,61 +76,6 @@ class ApplicationLog {
trace: trace?.toString() ?? '',
));
}
static void writeRawFailure(String message, Map<String, dynamic> extraData) async {
try {
await Globals().init();
final fn = path.join(Globals().rawFailureLogsDir.path, 'failure-${DateTime.now().toIso8601String()}.log');
var txt = "[TEXT]\n${message}\n\n";
for (var k in extraData.keys) {
txt += "[${k}]\n${_debugToStr(extraData[k])}\n\n";
}
await File(fn).writeAsString(txt);
ApplicationLog.debug("Wrote raw failure log to '${fn}' ('${message}')");
} catch (e) {
print("Failed to <writeRawFailure>: ${e}");
}
}
static String _debugToStr(dynamic v) {
if (v is String) {
return v;
}
if (v is StackTrace) {
return v.toString();
}
try {
final enc = new JsonEncoder.withIndent(" ");
return enc.convert(v);
} catch (e) {
// ignore
}
try {
return jsonEncode(v);
} catch (e) {
// ignore
}
try {
return v.toString();
} catch (e) {
// ignore
}
try {
return "${v}";
} catch (e) {
// ignore
}
return "<[!]FAILED_TO_PRINT_OBJECT>";
}
}
@HiveType(typeId: 103)

View File

@@ -2,9 +2,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path/path.dart' as path;
class Globals {
static final Globals _singleton = Globals._internal();
@@ -28,9 +26,6 @@ class Globals {
late SharedPreferences sharedPrefs;
late Directory appDocumentsDir;
late Directory rawFailureLogsDir;
bool get isInitialized => _initialized;
Future<void> init() async {
@@ -66,11 +61,6 @@ class Globals {
this.sharedPrefs = await SharedPreferences.getInstance();
this.appDocumentsDir = await getApplicationDocumentsDirectory();
this.rawFailureLogsDir = Directory(path.join(Globals().appDocumentsDir.path, "rawlogs"));
await this.rawFailureLogsDir.create(recursive: true);
this._initialized = true;
}

View File

@@ -1,29 +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'),
),
],
),
);
}
}

View File

@@ -72,7 +72,7 @@ class UI {
splashColor: Theme.of(context).splashColor,
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.all(16),
child: child,
),
),

View File

@@ -9,7 +9,6 @@ import device_info_plus
import firebase_core
import firebase_messaging
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
@@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
flutter_launcher_icons: ^0.14.3
flutter_launcher_icons: "^0.13.1"
font_awesome_flutter: '>= 4.7.0'
cupertino_icons: ^1.0.2
@@ -21,23 +21,21 @@ dependencies:
qr_flutter: ^4.1.0
url_launcher: ^6.2.4
infinite_scroll_pagination: ^4.0.0
intl: ^0.20.2
intl: ^0.19.0
path_provider: ^2.1.3
hive_flutter: ^1.1.0
package_info_plus: ^8.0.0
xid: ^1.2.1
flutter_lazy_indexed_stack: ^0.0.6
firebase_core: ^3.13.0
firebase_messaging: ^15.2.5
device_info_plus: ^11.3.0
toastification: ^3.0.1
firebase_core: ^2.32.0
firebase_messaging: ^14.9.4
device_info_plus: ^10.1.0
toastification: ^2.0.0
uuid: ^4.4.0
share_plus: ^10.1.4
flutter_local_notifications: ^17.2.3
share_plus: ^9.0.0
flutter_local_notifications: ^17.1.2
path: any
mobile_scanner: ^6.0.1
dependency_overrides:
font_awesome_flutter:
path: deps/font_awesome_flutter
@@ -47,7 +45,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_lints: ^4.0.0
hive_generator: ^2.0.1
build_runner: ^2.1.4

View File

@@ -411,6 +411,7 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo
type query struct {
PageSize *int `json:"page_size" form:"page_size"`
NextPageToken *string `json:"next_page_token" form:"next_page_token"`
Filter *string `json:"filter" form:"filter"`
Trimmed *bool `json:"trimmed" form:"trimmed"`
}
type response struct {

View File

@@ -73,7 +73,7 @@ var configLocHost = func() Config {
Journal: "WAL",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -86,7 +86,7 @@ var configLocHost = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -99,7 +99,7 @@ var configLocHost = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -145,7 +145,7 @@ var configLocDocker = func() Config {
Journal: "WAL",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -158,7 +158,7 @@ var configLocDocker = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -171,7 +171,7 @@ var configLocDocker = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -216,7 +216,7 @@ var configDev = func() Config {
Journal: "WAL",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -229,7 +229,7 @@ var configDev = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -242,7 +242,7 @@ var configDev = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -288,7 +288,7 @@ var configStag = func() Config {
Journal: "WAL",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -301,7 +301,7 @@ var configStag = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -314,7 +314,7 @@ var configStag = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -360,7 +360,7 @@ var configProd = func() Config {
Journal: "WAL",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -373,7 +373,7 @@ var configProd = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,
@@ -386,7 +386,7 @@ var configProd = func() Config {
Journal: "DELETE",
Timeout: 5 * time.Second,
CheckForeignKeys: false,
SingleConn: false,
SingleConn: true,
MaxOpenConns: 5,
MaxIdleConns: 5,
ConnMaxLifetime: 60 * time.Minute,

View File

@@ -23,7 +23,6 @@ type ChannelPreview struct {
InternalName string `json:"internal_name"`
DisplayName string `json:"display_name"`
DescriptionName *string `json:"description_name"`
MessagesSent int `json:"messages_sent"`
}
func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription {
@@ -40,6 +39,5 @@ func (c Channel) Preview() ChannelPreview {
InternalName: c.InternalName,
DisplayName: c.DisplayName,
DescriptionName: c.DescriptionName,
MessagesSent: c.MessagesSent,
}
}

View File

@@ -11,7 +11,7 @@ type User struct {
TimestampLastRead *SCNTime `db:"timestamp_lastread" json:"timestamp_lastread"`
TimestampLastSent *SCNTime `db:"timestamp_lastsent" json:"timestamp_lastsent"`
MessagesSent int `db:"messages_sent" json:"messages_sent"`
QuotaUsed int `db:"quota_used" json:"-"`
QuotaUsed int `db:"quota_used" json:"quota_used"`
QuotaUsedDay *string `db:"quota_used_day" json:"-"`
IsPro bool `db:"is_pro" json:"is_pro"`
ProToken *string `db:"pro_token" json:"-"`
@@ -22,7 +22,6 @@ type User struct {
type UserExtra struct {
QuotaRemaining int `json:"quota_remaining"`
QuotaPerDay int `json:"quota_max"`
QuotaUsed int `json:"quota_used"`
DefaultChannel string `json:"default_channel"`
MaxBodySize int `json:"max_body_size"`
MaxTitleLength int `json:"max_title_length"`
@@ -59,7 +58,6 @@ func (u User) WithClients(clients []Client, ak string, sk string, rk string) Use
func (u *User) PreMarshal() User {
u.UserExtra = UserExtra{
QuotaPerDay: u.QuotaPerDay(),
QuotaUsed: u.QuotaUsedToday(),
QuotaRemaining: u.QuotaRemainingToday(),
DefaultChannel: u.DefaultChannel(),
MaxBodySize: u.MaxContentLength(),