6 Commits

Author SHA1 Message Date
9c53cc52e9 Return messages_sent`from channel-preview
Some checks failed
Build Docker and Deploy / Build Docker Container (push) Successful in 1m22s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 9m38s
Build Docker and Deploy / Deploy to Server (push) Has been skipped
2025-04-12 20:30:19 +02:00
e6709cd4af implement changing username 2025-04-12 14:35:08 +02:00
cdb92757aa Fix quota_used returning old value when there were no messages today 2025-04-12 14:04:22 +02:00
3c5da802a7 Add format+mono mode to debug request view 2025-04-12 14:03:56 +02:00
05e2fcf185 Upgrade dependencies, android sdk, flutter, gradle, etc 2025-04-12 12:12:49 +02:00
8ebd95a4b8 fix build 2024-10-23 15:22:08 +02:00
25 changed files with 434 additions and 519 deletions

6
android/.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

263
android/.idea/other.xml generated
View File

@@ -1,263 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="q2q" />
<option name="id" value="q2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold3" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1768" />
<option name="screenY" value="2208" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="x1q" />
<option name="id" value="x1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S20" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1440" />
<option name="screenY" value="3200" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

17
android/.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

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

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.blackforestbytes.simplecloudnotifier">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

View File

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

View File

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

View File

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

2
flutter/.gitignore vendored
View File

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

View File

@@ -8,10 +8,17 @@
run: # runs app locally (linux)
flutter pub run build_runner build run-linux:
_JAVA_OPTIONS="" flutter run dart run build_runner build
_JAVA_OPTIONS="" flutter run -d linux
# runs app locally (web | not really supported)
run-linux:
dart run build_runner build
_JAVA_OPTIONS="" flutter run -d web
# runs on android device (must have network adb enabled teh correct IP)
run-android: run-android:
ping -c1 10.10.10.177 ping -c1 10.10.10.177
adb connect 10.10.10.177:5555 adb connect 10.10.10.177:5555
@@ -36,9 +43,10 @@ fix:
gen: gen:
flutter pub run build_runner build flutter pub run build_runner build
# run `make run` in another terminal (or another variant of flutter run)
autoreload: autoreload:
@# run `make run` in another terminal (or another variant of flutter run) @
@_utils/autoreload.sh @_utils/autoreload.sh
icons: icons:
@@ -46,4 +54,13 @@ 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

View File

@@ -27,6 +27,13 @@
- [ ] 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
- [ ] Add scrollbar
-> https://api.flutter.dev/flutter/material/Scrollbar-class.html
- [ ] you cant unsubscribe from foreign channel without completely loosing subscription.
perhaps subscriptions should have two cofirmed bool (both must be true to receive messages): confirmed-owner && confirmed-subscriber
Then the subscriber can unconfirm his half - without loosing the owner confirmation
----- -----
# TODO iOS specific # TODO iOS specific

View File

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

View File

@@ -34,7 +34,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
namespace "com.blackforestbytes.simplecloudnotifier" namespace "com.blackforestbytes.simplecloudnotifier"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion "27.0.12077973" // should be `flutter.ndkVersion` - but some plugins need 27, even though flutter still has the default value of 26 (flutter 3.29 | 2025-04-12)
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true

View File

@@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()

View File

@@ -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-7.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

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

View File

@@ -162,6 +162,20 @@ class APIClient {
); );
} }
static Future<User> updateUser(TokenSource auth, String uid, {String? username, String? proToken}) async {
return await _request(
name: 'updateUser',
method: 'PATCH',
relURL: 'users/$uid',
jsonBody: {
if (username != null) 'username': username,
if (proToken != null) 'pro_token': proToken,
},
fn: User.fromJson,
authToken: auth.getToken(),
);
}
static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async { static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
return await _request( return await _request(
name: 'addClient', name: 'addClient',

View File

@@ -13,6 +13,7 @@ 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';
@@ -359,13 +360,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
UI.buttonIconOnly( UI.buttonIconOnly(
onPressed: () {/*TODO*/}, onPressed: _changeUsername,
icon: FontAwesomeIcons.pen, icon: FontAwesomeIcons.pen,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if (!user.isPro) if (!user.isPro)
UI.buttonIconOnly( UI.buttonIconOnly(
onPressed: () {/*TODO*/}, onPressed: () {
Toaster.info("Not Implemented", "Account Upgrading will be implemented in a later version"); // TODO
},
icon: FontAwesomeIcons.cartCircleArrowUp, icon: FontAwesomeIcons.cartCircleArrowUp,
), ),
], ],
@@ -502,4 +505,31 @@ class _AccountRootPageState extends State<AccountRootPage> {
void _deleteAccount() async { void _deleteAccount() async {
//TODO //TODO
} }
void _changeUsername() async {
final acc = AppAuth();
if (!acc.isAuth()) return;
var newusername = await UIDialogs.showTextInput(context, 'Change your public username', 'Enter new username');
if (newusername == null) return;
newusername = newusername.trim();
if (newusername == '') {
Toaster.error("Error", 'Username cannot be empty');
return;
}
try {
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
setState(() {
futureUser = ImmediateFuture.ofValue(user);
});
Toaster.success("Success", 'Username changed');
_backgroundRefresh();
} catch (exc, trace) {
Toaster.error("Error", 'Failed to update username');
ApplicationLog.error('Failed to update username: ' + exc.toString(), trace: trace);
}
}
} }

View File

@@ -547,7 +547,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
final acc = AppAuth(); final acc = AppAuth();
try { try {
await APIClient.unconfirmSubscription(acc, widget.channelID, subscription!.subscriptionID); await APIClient.unconfirmSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call(); widget.needsReload?.call();
await _initStateAsync(false); await _initStateAsync(false);
@@ -563,7 +563,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
final acc = AppAuth(); final acc = AppAuth();
try { try {
await APIClient.confirmSubscription(acc, widget.channelID, subscription!.subscriptionID); await APIClient.confirmSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call(); widget.needsReload?.call();
await _initStateAsync(false); await _initStateAsync(false);
@@ -579,7 +579,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
final acc = AppAuth(); final acc = AppAuth();
try { try {
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID); await APIClient.deleteSubscription(acc, widget.channelID, sub.subscriptionID);
widget.needsReload?.call(); widget.needsReload?.call();
await _initStateAsync(false); await _initStateAsync(false);

View File

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

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class UIDialogs {
static Future<String?> showTextInput(BuildContext context, String title, String hintText) {
var _textFieldController = TextEditingController();
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: TextField(
autofocus: true,
controller: _textFieldController,
decoration: InputDecoration(hintText: hintText),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(_textFieldController.text),
child: Text('OK'),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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