Finish KeyToken operations
This commit is contained in:
parent
1f0f280286
commit
78c895547e
@ -445,6 +445,16 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteKeyToken(AppAuth acc, String keytokenID) {
|
||||||
|
return _request(
|
||||||
|
name: 'deleteKeyToken',
|
||||||
|
method: 'DELETE',
|
||||||
|
relURL: 'users/${acc.getUserID()}/keys/${keytokenID}',
|
||||||
|
fn: (_) => null,
|
||||||
|
authToken: acc.getToken(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
|
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'updateKeyToken',
|
name: 'updateKeyToken',
|
||||||
@ -468,7 +478,7 @@ class APIClient {
|
|||||||
relURL: 'users/${auth.getUserID()}/keys',
|
relURL: 'users/${auth.getUserID()}/keys',
|
||||||
jsonBody: {
|
jsonBody: {
|
||||||
'name': name,
|
'name': name,
|
||||||
'pem': perm,
|
'permissions': perm,
|
||||||
'all_channels': allChannels,
|
'all_channels': allChannels,
|
||||||
if (channels != null) 'channels': channels,
|
if (channels != null) 'channels': channels,
|
||||||
},
|
},
|
||||||
|
51
flutter/lib/components/badge_display/badge_display.dart
Normal file
51
flutter/lib/components/badge_display/badge_display.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
enum BadgeMode { error, warn, info }
|
||||||
|
|
||||||
|
class BadgeDisplay extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final BadgeMode mode;
|
||||||
|
final IconData? icon;
|
||||||
|
|
||||||
|
const BadgeDisplay({
|
||||||
|
Key? key,
|
||||||
|
required this.text,
|
||||||
|
required this.mode,
|
||||||
|
required this.icon,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var col = Colors.grey;
|
||||||
|
var colFG = Colors.black;
|
||||||
|
|
||||||
|
if (mode == BadgeMode.error) col = Colors.red;
|
||||||
|
if (mode == BadgeMode.warn) col = Colors.orange;
|
||||||
|
if (mode == BadgeMode.info) col = Colors.blue;
|
||||||
|
|
||||||
|
if (mode == BadgeMode.error) colFG = Colors.red[900]!;
|
||||||
|
if (mode == BadgeMode.warn) colFG = Colors.black;
|
||||||
|
if (mode == BadgeMode.info) colFG = Colors.black;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: col[100],
|
||||||
|
border: Border.all(color: col[300]!),
|
||||||
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (icon != null) Icon(icon!, color: colFG, size: 16.0),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: colFG, fontSize: 14.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -17,13 +17,12 @@ class FilterModalChannel extends StatefulWidget {
|
|||||||
class _FilterModalChannelState extends State<FilterModalChannel> {
|
class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
late ImmediateFuture<List<Channel>>? _futureChannels;
|
ImmediateFuture<List<Channel>> _futureChannels = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_futureChannels = null;
|
|
||||||
_futureChannels = ImmediateFuture.ofFuture(() async {
|
_futureChannels = ImmediateFuture.ofFuture(() async {
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
@ -51,45 +50,39 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: () {
|
child: FutureBuilder(
|
||||||
if (_futureChannels == null) {
|
future: _futureChannels.future,
|
||||||
return Center(child: CircularProgressIndicator());
|
builder: ((context, snapshot) {
|
||||||
}
|
if (_futureChannels.value != null) {
|
||||||
|
return _buildList(context, _futureChannels.value!);
|
||||||
return FutureBuilder(
|
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
future: _futureChannels!.future,
|
return Center(child: CircularProgressIndicator());
|
||||||
builder: ((context, snapshot) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
if (_futureChannels?.value != null) {
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||||
return _buildList(context, _futureChannels!.value!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
return _buildList(context, snapshot.data!);
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
} else {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||||
return _buildList(context, snapshot.data!);
|
}
|
||||||
} else {
|
}),
|
||||||
return Center(child: CircularProgressIndicator());
|
),
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}(),
|
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
child: const Text('Apply'),
|
child: const Text('Apply'),
|
||||||
onPressed: () {
|
onPressed: _onOkay,
|
||||||
onOkay();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onOkay() {
|
void _onOkay() {
|
||||||
Navi.popDialog(context);
|
Navi.popDialog(context);
|
||||||
|
|
||||||
final chiplets = _selectedEntries
|
final chiplets = _selectedEntries
|
||||||
.map((e) => MessageFilterChiplet(
|
.map((e) => MessageFilterChiplet(
|
||||||
label: _futureChannels?.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
label: _futureChannels.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???',
|
||||||
value: e,
|
value: e,
|
||||||
type: MessageFilterChipletType.channel,
|
type: MessageFilterChipletType.channel,
|
||||||
))
|
))
|
||||||
|
@ -17,13 +17,12 @@ class FilterModalKeytoken extends StatefulWidget {
|
|||||||
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
late ImmediateFuture<List<KeyToken>>? _futureKeyTokens;
|
ImmediateFuture<List<KeyToken>> _futureKeyTokens = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_futureKeyTokens = null;
|
|
||||||
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
@ -51,26 +50,22 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: () {
|
child: FutureBuilder(
|
||||||
if (_futureKeyTokens == null) {
|
future: _futureKeyTokens.future,
|
||||||
return Center(child: CircularProgressIndicator());
|
builder: ((context, snapshot) {
|
||||||
}
|
if (_futureKeyTokens.value != null) {
|
||||||
|
return _buildList(context, _futureKeyTokens.value!);
|
||||||
return FutureBuilder(
|
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
future: _futureKeyTokens!.future,
|
return Center(child: CircularProgressIndicator());
|
||||||
builder: ((context, snapshot) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
if (_futureKeyTokens?.value != null) {
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||||
return _buildList(context, _futureKeyTokens!.value!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
return _buildList(context, snapshot.data!);
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
} else {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||||
return _buildList(context, snapshot.data!);
|
}
|
||||||
} else {
|
}),
|
||||||
return Center(child: CircularProgressIndicator());
|
),
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}(),
|
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -89,7 +84,7 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
|||||||
|
|
||||||
final chiplets = _selectedEntries
|
final chiplets = _selectedEntries
|
||||||
.map((e) => MessageFilterChiplet(
|
.map((e) => MessageFilterChiplet(
|
||||||
label: _futureKeyTokens?.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
label: _futureKeyTokens.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???',
|
||||||
value: e,
|
value: e,
|
||||||
type: MessageFilterChipletType.sender,
|
type: MessageFilterChipletType.sender,
|
||||||
))
|
))
|
||||||
|
@ -15,13 +15,12 @@ class FilterModalSendername extends StatefulWidget {
|
|||||||
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||||
Set<String> _selectedEntries = {};
|
Set<String> _selectedEntries = {};
|
||||||
|
|
||||||
late ImmediateFuture<List<String>>? _futureSenders;
|
ImmediateFuture<List<String>> _futureSenders = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_futureSenders = null;
|
|
||||||
_futureSenders = ImmediateFuture.ofFuture(() async {
|
_futureSenders = ImmediateFuture.ofFuture(() async {
|
||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
@ -49,26 +48,22 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
|
|||||||
content: Container(
|
content: Container(
|
||||||
width: 9000,
|
width: 9000,
|
||||||
height: 9000,
|
height: 9000,
|
||||||
child: () {
|
child: FutureBuilder(
|
||||||
if (_futureSenders == null) {
|
future: _futureSenders.future,
|
||||||
return Center(child: CircularProgressIndicator());
|
builder: ((context, snapshot) {
|
||||||
}
|
if (_futureSenders.value != null) {
|
||||||
|
return _buildList(context, _futureSenders.value!);
|
||||||
return FutureBuilder(
|
} else if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
future: _futureSenders!.future,
|
return Center(child: CircularProgressIndicator());
|
||||||
builder: ((context, snapshot) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
if (_futureSenders?.value != null) {
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||||
return _buildList(context, _futureSenders!.value!);
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
return _buildList(context, snapshot.data!);
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
} else {
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
return ErrorDisplay(errorMessage: 'Invalid future state');
|
||||||
return _buildList(context, snapshot.data!);
|
}
|
||||||
} else {
|
}),
|
||||||
return Center(child: CircularProgressIndicator());
|
),
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}(),
|
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -9,6 +9,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
|||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/client.dart';
|
import 'package:simplecloudnotifier/models/client.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/nav_layout.dart';
|
import 'package:simplecloudnotifier/nav_layout.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||||
@ -50,6 +51,7 @@ void main() async {
|
|||||||
Hive.registerAdapter(SCNMessageAdapter());
|
Hive.registerAdapter(SCNMessageAdapter());
|
||||||
Hive.registerAdapter(ChannelAdapter());
|
Hive.registerAdapter(ChannelAdapter());
|
||||||
Hive.registerAdapter(FBMessageAdapter());
|
Hive.registerAdapter(FBMessageAdapter());
|
||||||
|
Hive.registerAdapter(KeyTokenAdapter());
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-logs>...');
|
print('[INIT] Load Hive<scn-logs>...');
|
||||||
|
|
||||||
@ -106,6 +108,17 @@ void main() async {
|
|||||||
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
|
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[INIT] Load Hive<scn-keytoken-value-cache>...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Hive.deleteBoxFromDisk('scn-keytoken-value-cache');
|
||||||
|
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
|
||||||
|
ApplicationLog.error('Failed to open Hive-Box: scn-keytoken-value-cache' + exc.toString(), trace: trace);
|
||||||
|
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-keytoken-value-cache', {'error': exc.toString(), 'trace': trace});
|
||||||
|
}
|
||||||
|
|
||||||
print('[INIT] Load AppAuth...');
|
print('[INIT] Load AppAuth...');
|
||||||
|
|
||||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||||
|
@ -1,12 +1,27 @@
|
|||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
|
||||||
|
part 'keytoken.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 107)
|
||||||
class KeyToken {
|
class KeyToken {
|
||||||
|
@HiveField(0)
|
||||||
final String keytokenID;
|
final String keytokenID;
|
||||||
|
|
||||||
|
@HiveField(10)
|
||||||
final String name;
|
final String name;
|
||||||
|
@HiveField(11)
|
||||||
final String timestampCreated;
|
final String timestampCreated;
|
||||||
|
@HiveField(13)
|
||||||
final String? timestampLastUsed;
|
final String? timestampLastUsed;
|
||||||
|
@HiveField(14)
|
||||||
final String ownerUserID;
|
final String ownerUserID;
|
||||||
|
@HiveField(15)
|
||||||
final bool allChannels;
|
final bool allChannels;
|
||||||
|
@HiveField(16)
|
||||||
final List<String> channels;
|
final List<String> channels;
|
||||||
|
@HiveField(17)
|
||||||
final String permissions;
|
final String permissions;
|
||||||
|
@HiveField(18)
|
||||||
final int messagesSent;
|
final int messagesSent;
|
||||||
|
|
||||||
const KeyToken({
|
const KeyToken({
|
||||||
|
65
flutter/lib/models/keytoken.g.dart
Normal file
65
flutter/lib/models/keytoken.g.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'keytoken.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class KeyTokenAdapter extends TypeAdapter<KeyToken> {
|
||||||
|
@override
|
||||||
|
final int typeId = 107;
|
||||||
|
|
||||||
|
@override
|
||||||
|
KeyToken read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return KeyToken(
|
||||||
|
keytokenID: fields[0] as String,
|
||||||
|
name: fields[10] as String,
|
||||||
|
timestampCreated: fields[11] as String,
|
||||||
|
timestampLastUsed: fields[13] as String?,
|
||||||
|
ownerUserID: fields[14] as String,
|
||||||
|
allChannels: fields[15] as bool,
|
||||||
|
channels: (fields[16] as List).cast<String>(),
|
||||||
|
permissions: fields[17] as String,
|
||||||
|
messagesSent: fields[18] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, KeyToken obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(9)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.keytokenID)
|
||||||
|
..writeByte(10)
|
||||||
|
..write(obj.name)
|
||||||
|
..writeByte(11)
|
||||||
|
..write(obj.timestampCreated)
|
||||||
|
..writeByte(13)
|
||||||
|
..write(obj.timestampLastUsed)
|
||||||
|
..writeByte(14)
|
||||||
|
..write(obj.ownerUserID)
|
||||||
|
..writeByte(15)
|
||||||
|
..write(obj.allChannels)
|
||||||
|
..writeByte(16)
|
||||||
|
..write(obj.channels)
|
||||||
|
..writeByte(17)
|
||||||
|
..write(obj.permissions)
|
||||||
|
..writeByte(18)
|
||||||
|
..write(obj.messagesSent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is KeyTokenAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
@ -35,14 +35,13 @@ class AccountRootPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AccountRootPageState extends State<AccountRootPage> {
|
class _AccountRootPageState extends State<AccountRootPage> {
|
||||||
late ImmediateFuture<int>? futureSubscriptionCount;
|
ImmediateFuture<int> _futureSubscriptionCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureClientCount;
|
ImmediateFuture<int> _futureClientCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureKeyCount;
|
ImmediateFuture<int> _futureKeyCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureChannelAllCount;
|
ImmediateFuture<int> _futureChannelAllCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
ImmediateFuture<int> _futureChannelOwnedCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureChannelOwnedCount;
|
ImmediateFuture<int> _futureSenderNamesCount = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<int>? futureSenderNamesCount;
|
ImmediateFuture<User> _futureUser = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<User>? futureUser;
|
|
||||||
|
|
||||||
late AppAuth userAcc;
|
late AppAuth userAcc;
|
||||||
|
|
||||||
@ -92,58 +91,51 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _createFutures() {
|
void _createFutures() {
|
||||||
futureSubscriptionCount = null;
|
_futureSubscriptionCount = ImmediateFuture.ofPending();
|
||||||
futureClientCount = null;
|
_futureClientCount = ImmediateFuture.ofPending();
|
||||||
futureKeyCount = null;
|
_futureKeyCount = ImmediateFuture.ofPending();
|
||||||
futureChannelAllCount = null;
|
_futureChannelAllCount = ImmediateFuture.ofPending();
|
||||||
futureChannelSubscribedCount = null;
|
_futureChannelOwnedCount = ImmediateFuture.ofPending();
|
||||||
futureChannelOwnedCount = null;
|
_futureSenderNamesCount = ImmediateFuture.ofPending();
|
||||||
futureSenderNamesCount = null;
|
|
||||||
|
|
||||||
if (userAcc.isAuth()) {
|
if (userAcc.isAuth()) {
|
||||||
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
_futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async {
|
_futureChannelOwnedCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
|
||||||
return channels.length;
|
|
||||||
}());
|
|
||||||
|
|
||||||
futureChannelOwnedCount = ImmediateFuture.ofFuture(() async {
|
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
_futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
return subs.length;
|
return subs.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureClientCount = ImmediateFuture.ofFuture(() async {
|
_futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
return clients.length;
|
return clients.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureKeyCount = ImmediateFuture.ofFuture(() async {
|
_futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
return keys.length;
|
return keys.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
|
_futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
|
||||||
return senders.length;
|
return senders.length;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
_futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +149,6 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
// refresh all data and then replace teh futures used in build()
|
// refresh all data and then replace teh futures used in build()
|
||||||
|
|
||||||
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||||
final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
|
||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
@ -165,13 +156,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
final user = await userAcc.loadUser(force: true);
|
final user = await userAcc.loadUser(force: true);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
_futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
||||||
futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length);
|
_futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||||
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
_futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||||
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
_futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||||
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
_futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
||||||
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
|
_futureUser = ImmediateFuture.ofValue(user);
|
||||||
futureUser = ImmediateFuture.ofValue(user);
|
|
||||||
});
|
});
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
||||||
@ -192,10 +182,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
return _buildNoAuth(context);
|
return _buildNoAuth(context);
|
||||||
} else {
|
} else {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: futureUser!.future,
|
future: _futureUser.future,
|
||||||
builder: ((context, snapshot) {
|
builder: ((context, snapshot) {
|
||||||
if (futureUser?.value != null) {
|
if (_futureUser.value != null) {
|
||||||
return _buildShowAccount(context, acc, futureUser!.value!);
|
return _buildShowAccount(context, acc, _futureUser.value!);
|
||||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
@ -354,10 +344,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: futureChannelOwnedCount!.future,
|
future: _futureChannelOwnedCount.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (futureChannelOwnedCount?.value != null) {
|
if (_futureChannelOwnedCount.value != null) {
|
||||||
return Text('${futureChannelOwnedCount!.value}');
|
return Text('${_futureChannelOwnedCount.value}');
|
||||||
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
|
return Text('ERROR: ${snapshot.error}', style: TextStyle(color: Colors.red));
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return Text('${snapshot.data}');
|
return Text('${snapshot.data}');
|
||||||
} else {
|
} else {
|
||||||
@ -393,11 +385,11 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
|
|
||||||
List<Widget> _buildCards(BuildContext context, User user) {
|
List<Widget> _buildCards(BuildContext context, User user) {
|
||||||
return [
|
return [
|
||||||
_buildNumberCard(context, 'Subscription', 's', futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
|
_buildNumberCard(context, 'Subscription', 's', _futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
|
||||||
_buildNumberCard(context, 'Client', 's', futureClientCount, () => Navi.push(context, () => ClientListPage())),
|
_buildNumberCard(context, 'Client', 's', _futureClientCount, () => Navi.push(context, () => ClientListPage())),
|
||||||
_buildNumberCard(context, 'Key', 's', futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
|
_buildNumberCard(context, 'Key', 's', _futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
|
||||||
_buildNumberCard(context, 'Channel', 's', futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
|
_buildNumberCard(context, 'Channel', 's', _futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
|
||||||
_buildNumberCard(context, 'Sender', '', futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
|
_buildNumberCard(context, 'Sender', '', _futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
|
||||||
UI.buttonCard(
|
UI.buttonCard(
|
||||||
context: context,
|
context: context,
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||||
@ -415,17 +407,19 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int>? future, void Function() action) {
|
Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int> future, void Function() action) {
|
||||||
return UI.buttonCard(
|
return UI.buttonCard(
|
||||||
context: context,
|
context: context,
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: future?.future,
|
future: future.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (future?.value != null) {
|
if (future.value != null) {
|
||||||
return Text('${future!.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${future.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
|
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
} else {
|
} else {
|
||||||
@ -435,10 +429,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: future?.future,
|
future: future.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (future?.value != null) {
|
if (future.value != null) {
|
||||||
return Text('${txt}${((future!.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${txt}${((future.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
|
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||||
|
return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red));
|
||||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||||
return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||||
} else {
|
} else {
|
||||||
@ -562,7 +558,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
try {
|
try {
|
||||||
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
|
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
|
||||||
setState(() {
|
setState(() {
|
||||||
futureUser = ImmediateFuture.ofValue(user);
|
_futureUser = ImmediateFuture.ofValue(user);
|
||||||
});
|
});
|
||||||
Toaster.success("Success", 'Username changed');
|
Toaster.success("Success", 'Username changed');
|
||||||
|
|
||||||
|
@ -44,9 +44,9 @@ enum EditState { none, editing, saving }
|
|||||||
enum ChannelViewPageInitState { loading, okay, error }
|
enum ChannelViewPageInitState { loading, okay, error }
|
||||||
|
|
||||||
class _ChannelViewPageState extends State<ChannelViewPage> {
|
class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||||
late ImmediateFuture<String?> _futureSubscribeKey;
|
ImmediateFuture<String?> _futureSubscribeKey = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions;
|
ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<UserPreview> _futureOwner;
|
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
final TextEditingController _ctrlDisplayName = TextEditingController();
|
final TextEditingController _ctrlDisplayName = TextEditingController();
|
||||||
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
||||||
|
@ -32,24 +32,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
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()), 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), 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), 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), Divider(), UI.button(text: "Copy as curl", onPressed: _copyCurl, tonal: true)],
|
||||||
...buildRow(context, "name", "Name", widget.request.name),
|
|
||||||
...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()),
|
|
||||||
...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()),
|
|
||||||
...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()),
|
|
||||||
Divider(),
|
|
||||||
...buildRow(context, "method", "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),
|
|
||||||
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),
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -130,4 +113,18 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _copyCurl() {
|
||||||
|
final method = '-X ${widget.request.method}';
|
||||||
|
final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' ');
|
||||||
|
final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : '';
|
||||||
|
|
||||||
|
final curlParts = ['curl', method, header, '"${widget.request.url}"', body];
|
||||||
|
|
||||||
|
final txt = curlParts.where((part) => part.isNotEmpty).join(' ');
|
||||||
|
|
||||||
|
Clipboard.setData(new ClipboardData(text: txt));
|
||||||
|
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||||
|
print('================= [CLIPBOARD] =================\n${txt}\n================= [/CLIPBOARD] =================');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,10 +47,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
@ -61,7 +57,14 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(req.type),
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||||
|
Expanded(child: SizedBox()),
|
||||||
|
Text(req.type),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
req.error,
|
req.error,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -81,10 +84,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
@ -92,7 +91,13 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
|
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
subtitle: Text(req.type),
|
subtitle: Row(
|
||||||
|
children: [
|
||||||
|
Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
|
||||||
|
Expanded(child: SizedBox()),
|
||||||
|
Text(req.type),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal file
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
|
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
|
||||||
|
class KeyTokenCreateDialog extends StatefulWidget {
|
||||||
|
final void Function(KeyToken, String) onCreated;
|
||||||
|
|
||||||
|
const KeyTokenCreateDialog({
|
||||||
|
required this.onCreated,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_KeyTokenCreateDialogState createState() => _KeyTokenCreateDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeyTokenCreateDialogState extends State<KeyTokenCreateDialog> {
|
||||||
|
TextEditingController _ctrlName = TextEditingController();
|
||||||
|
Set<String> selectedPermissions = {'CS'};
|
||||||
|
|
||||||
|
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
|
bool allChannels = true;
|
||||||
|
Set<String> selectedChannels = new Set<String>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrlName.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Create new key'),
|
||||||
|
content: Container(
|
||||||
|
width: 0,
|
||||||
|
height: 400,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildNameCtrl(context),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
_buildPermissionCtrl(context),
|
||||||
|
SizedBox(height: 32),
|
||||||
|
_buildChannelCtrl(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
|
child: const Text('Create'),
|
||||||
|
onPressed: _create,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNameCtrl(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: _ctrlName,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Key name',
|
||||||
|
hintText: 'Enter a name for the new key',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPermissionCtrl(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text('Permissions:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
primary: false,
|
||||||
|
itemBuilder: (builder, index) {
|
||||||
|
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
||||||
|
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text(txt),
|
||||||
|
leading: Icon(
|
||||||
|
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (selectedPermissions.contains(prm)) {
|
||||||
|
selectedPermissions.remove(prm);
|
||||||
|
} else {
|
||||||
|
selectedPermissions.add(prm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: 4,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChannelCtrl(BuildContext context) {
|
||||||
|
return FutureBuilder<List<Channel>>(
|
||||||
|
future: _futureOwnedChannels.future,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final ownChannels = snapshot.data!;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text('Channels:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text('All Channels'),
|
||||||
|
leading: Icon(
|
||||||
|
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
allChannels = !allChannels;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
if (!allChannels)
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
primary: false,
|
||||||
|
itemBuilder: (builder, index) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text(ownChannels[index].displayName),
|
||||||
|
leading: Icon(
|
||||||
|
selectedChannels.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (selectedChannels.contains(ownChannels[index].channelID)) {
|
||||||
|
selectedChannels.remove(ownChannels[index].channelID);
|
||||||
|
} else {
|
||||||
|
selectedChannels.add(ownChannels[index].channelID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: ownChannels.length,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _create() async {
|
||||||
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
if (!userAcc.isAuth()) return;
|
||||||
|
|
||||||
|
if (_ctrlName.text.isEmpty) {
|
||||||
|
Toaster.error('Missing data', 'Please enter a name for the key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final perm = selectedPermissions.join(';');
|
||||||
|
final channels = allChannels ? <String>[] : selectedChannels.toList();
|
||||||
|
|
||||||
|
var kt = await APIClient.createKeyToken(userAcc, _ctrlName.text, perm, allChannels, channels: channels);
|
||||||
|
Toaster.success('Success', 'Key created successfully');
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onCreated(kt.keyToken, kt.token);
|
||||||
|
} catch (exc, trace) {
|
||||||
|
ApplicationLog.error('Failed to create keytoken: ' + exc.toString(), trace: trace);
|
||||||
|
Toaster.error("Error", 'Failed to create key: ${exc.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal file
97
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:simplecloudnotifier/components/badge_display/badge_display.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
|
class KeyTokenCreatedModal extends StatelessWidget {
|
||||||
|
final KeyToken keytoken;
|
||||||
|
final String tokenValue;
|
||||||
|
|
||||||
|
const KeyTokenCreatedModal({
|
||||||
|
Key? key,
|
||||||
|
required this.keytoken,
|
||||||
|
required this.tokenValue,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('A new key was created'),
|
||||||
|
content: Container(
|
||||||
|
width: 0,
|
||||||
|
height: 350,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidIdCardClip,
|
||||||
|
title: 'KeyTokenID',
|
||||||
|
values: [keytoken.keytokenID],
|
||||||
|
),
|
||||||
|
UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidInputText,
|
||||||
|
title: 'Name',
|
||||||
|
values: [keytoken.name],
|
||||||
|
),
|
||||||
|
UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||||
|
title: 'Permissions',
|
||||||
|
values: _formatPermissions(keytoken.permissions),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const BadgeDisplay(
|
||||||
|
text: "Please copy and save the token now, it cannot be retrieved later.",
|
||||||
|
icon: null,
|
||||||
|
mode: BadgeMode.warn,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidKey,
|
||||||
|
title: 'Token',
|
||||||
|
values: [tokenValue.substring(0, 12) + '...'],
|
||||||
|
iconActions: [(FontAwesomeIcons.copy, null, _copy)],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Close'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _formatPermissions(String v) {
|
||||||
|
var splt = v.split(';');
|
||||||
|
|
||||||
|
if (splt.length == 0) return ["None"];
|
||||||
|
|
||||||
|
List<String> result = [];
|
||||||
|
|
||||||
|
if (splt.contains("A")) result.add("Admin");
|
||||||
|
if (splt.contains("UR")) result.add("Read Account");
|
||||||
|
if (splt.contains("CR")) result.add("Read Messages");
|
||||||
|
if (splt.contains("CS")) result.add("Send Messages");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copy() {
|
||||||
|
Clipboard.setData(new ClipboardData(text: tokenValue));
|
||||||
|
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
||||||
|
print('================= [CLIPBOARD] =================\n${tokenValue}\n================= [/CLIPBOARD] =================');
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
|
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
|
||||||
@ -81,6 +84,27 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
heroTag: 'fab_keytokenlist_plus',
|
||||||
|
onPressed: () {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => KeyTokenCreateDialog(onCreated: _created),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(FontAwesomeIcons.plus),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _created(KeyToken token, String tokValue) {
|
||||||
|
setState(() {
|
||||||
|
_pagingController.itemList?.insert(0, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => KeyTokenCreatedModal(keytoken: token, tokenValue: tokValue),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal file
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
|
||||||
|
class EditKeyTokenChannelsDialog extends StatefulWidget {
|
||||||
|
final List<Channel> ownedChannels;
|
||||||
|
final KeyTokenPreview keytoken;
|
||||||
|
|
||||||
|
final void Function(Set<String>) onUpdateChannels;
|
||||||
|
final void Function() onUpdateSetAllChannels;
|
||||||
|
|
||||||
|
const EditKeyTokenChannelsDialog({
|
||||||
|
required this.ownedChannels,
|
||||||
|
required this.keytoken,
|
||||||
|
required this.onUpdateChannels,
|
||||||
|
required this.onUpdateSetAllChannels,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EditKeyTokenChannelsDialogState createState() => _EditKeyTokenChannelsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditKeyTokenChannelsDialogState extends State<EditKeyTokenChannelsDialog> {
|
||||||
|
late bool allChannels;
|
||||||
|
late Set<String> selectedEntries;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
allChannels = widget.keytoken.allChannels;
|
||||||
|
selectedEntries = (widget.keytoken.channels).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var ownChannels = widget.ownedChannels.toList();
|
||||||
|
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Channels'),
|
||||||
|
content: Container(
|
||||||
|
width: 0,
|
||||||
|
height: 400,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text('All Channels'),
|
||||||
|
leading: Icon(
|
||||||
|
allChannels ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
allChannels = !allChannels;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
if (!allChannels)
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (builder, index) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text(ownChannels[index].displayName),
|
||||||
|
leading: Icon(
|
||||||
|
selectedEntries.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (selectedEntries.contains(ownChannels[index].channelID)) {
|
||||||
|
selectedEntries.remove(ownChannels[index].channelID);
|
||||||
|
} else {
|
||||||
|
selectedEntries.add(ownChannels[index].channelID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: ownChannels.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
|
child: const Text('Update'),
|
||||||
|
onPressed: () {
|
||||||
|
if (allChannels) {
|
||||||
|
widget.onUpdateSetAllChannels();
|
||||||
|
} else {
|
||||||
|
widget.onUpdateChannels(selectedEntries);
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
|
||||||
|
class EditKeyTokenPermissionsDialog extends StatefulWidget {
|
||||||
|
final KeyTokenPreview keytoken;
|
||||||
|
|
||||||
|
final void Function(String) onUpdatePermissions;
|
||||||
|
|
||||||
|
const EditKeyTokenPermissionsDialog({
|
||||||
|
required this.keytoken,
|
||||||
|
required this.onUpdatePermissions,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_EditKeyTokenPermissionsDialogState createState() => _EditKeyTokenPermissionsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditKeyTokenPermissionsDialogState extends State<EditKeyTokenPermissionsDialog> {
|
||||||
|
Set<String> selectedPermissions = new Set<String>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
for (var p in widget.keytoken.permissions.split(';')) {
|
||||||
|
if (p.isNotEmpty) selectedPermissions.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Permissions'),
|
||||||
|
content: Container(
|
||||||
|
width: 0,
|
||||||
|
height: 400,
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (builder, index) {
|
||||||
|
final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index];
|
||||||
|
final prm = (['A', 'CR', 'CS', 'UR'])[index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||||
|
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
|
||||||
|
title: Text(txt),
|
||||||
|
leading: Icon(
|
||||||
|
selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (selectedPermissions.contains(prm)) {
|
||||||
|
selectedPermissions.remove(prm);
|
||||||
|
} else {
|
||||||
|
selectedPermissions.add(prm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
|
||||||
|
child: const Text('Update'),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onUpdatePermissions(selectedPermissions.join(';'));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,23 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_channel_modal.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_permission_modal.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/scn_data_cache.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';
|
||||||
@ -40,7 +47,11 @@ enum KeyTokenViewPageInitState { loading, okay, error }
|
|||||||
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
||||||
|
|
||||||
late ImmediateFuture<UserPreview> _futureOwner;
|
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
|
ImmediateFuture<Map<String, ChannelPreview>> _futureAllChannels = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
|
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
final TextEditingController _ctrlName = TextEditingController();
|
final TextEditingController _ctrlName = TextEditingController();
|
||||||
|
|
||||||
@ -55,6 +66,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
|
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
|
||||||
String errorMessage = '';
|
String errorMessage = '';
|
||||||
|
|
||||||
|
KeyToken? keytokenUserAccAdmin;
|
||||||
|
KeyToken? keytokenUserAccSend;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_initStateAsync(true);
|
_initStateAsync(true);
|
||||||
@ -102,14 +116,48 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
|
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
|
||||||
var cacheUser = userAcc.getUserOrNull();
|
var cacheUser = userAcc.getUserOrNull();
|
||||||
if (cacheUser != null) {
|
if (cacheUser != null) {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
_futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview());
|
||||||
} else {
|
} else {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
|
_futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID));
|
_futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_futureAllChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.allAny).then((lst) async {
|
||||||
|
Map<String, ChannelPreview> result = {};
|
||||||
|
|
||||||
|
for (var c in lst) result[c.channel.channelID] = c.channel.toPreview(c.subscription);
|
||||||
|
|
||||||
|
if (keytokenPreview != null) {
|
||||||
|
for (var cid in keytokenPreview!.channels) {
|
||||||
|
if (!result.containsKey(cid)) {
|
||||||
|
result[cid] = await APIClient.getChannelPreview(userAcc, cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList()));
|
||||||
|
});
|
||||||
|
|
||||||
|
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenAdmin!).then((token) {
|
||||||
|
setState(() {
|
||||||
|
keytokenUserAccAdmin = token;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenSend!).then((token) {
|
||||||
|
setState(() {
|
||||||
|
keytokenUserAccSend = token;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -158,18 +206,22 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.solidIdCardClip,
|
icon: FontAwesomeIcons.solidIdCardClip,
|
||||||
title: 'KeyTokenID',
|
title: 'KeyTokenID',
|
||||||
values: [keytoken.keytokenID],
|
values: [
|
||||||
|
keytoken.keytokenID,
|
||||||
|
if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)',
|
||||||
|
if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)',
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_buildNameCard(context, true),
|
_buildNameCard(context, true),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.clock,
|
icon: FontAwesomeIcons.solidClock,
|
||||||
title: 'Created',
|
title: 'Created',
|
||||||
values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
|
values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
|
||||||
),
|
),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.clockTwo,
|
icon: FontAwesomeIcons.solidClockTwo,
|
||||||
title: 'Last Used',
|
title: 'Last Used',
|
||||||
values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
|
values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
|
||||||
),
|
),
|
||||||
@ -297,7 +349,6 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
|
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
|
||||||
_editName = EditState.editing;
|
_editName = EditState.editing;
|
||||||
if (_editName == EditState.editing) _editName = EditState.none;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,7 +406,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
if (isOwned) {
|
if (isOwned) {
|
||||||
w1 = UI.metaCard(
|
w1 = UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.shieldKeyhole,
|
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||||
title: 'Permissions',
|
title: 'Permissions',
|
||||||
values: _formatPermissions(keyToken.permissions),
|
values: _formatPermissions(keyToken.permissions),
|
||||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
|
iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
|
||||||
@ -363,28 +414,53 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
w1 = UI.metaCard(
|
w1 = UI.metaCard(
|
||||||
context: context,
|
context: context,
|
||||||
icon: FontAwesomeIcons.shieldKeyhole,
|
icon: FontAwesomeIcons.solidShieldKeyhole,
|
||||||
title: 'Permissions',
|
title: 'Permissions',
|
||||||
values: _formatPermissions(keyToken.permissions),
|
values: _formatPermissions(keyToken.permissions),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOwned) {
|
w2 = FutureBuilder(
|
||||||
w2 = UI.metaCard(
|
future: _futureAllChannels.future,
|
||||||
context: context,
|
builder: (context, snapshot) {
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
if (snapshot.hasData) {
|
||||||
title: 'Channels',
|
var cmap = snapshot.data!;
|
||||||
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names
|
if (isOwned) {
|
||||||
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
return UI.metaCard(
|
||||||
);
|
context: context,
|
||||||
} else {
|
icon: FontAwesomeIcons.solidSnake,
|
||||||
w2 = UI.metaCard(
|
title: 'Channels',
|
||||||
context: context,
|
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
||||||
icon: FontAwesomeIcons.solidSnake,
|
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
||||||
title: 'Channels',
|
);
|
||||||
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names
|
} else {
|
||||||
);
|
return UI.metaCard(
|
||||||
}
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidSnake,
|
||||||
|
title: 'Channels',
|
||||||
|
values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isOwned) {
|
||||||
|
return UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidSnake,
|
||||||
|
title: 'Channels',
|
||||||
|
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
||||||
|
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return UI.metaCard(
|
||||||
|
context: context,
|
||||||
|
icon: FontAwesomeIcons.solidSnake,
|
||||||
|
title: 'Channels',
|
||||||
|
values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return [w1, w2];
|
return [w1, w2];
|
||||||
}
|
}
|
||||||
@ -404,33 +480,138 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editPermissions() {
|
void _editPermissions() async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
//TODO prevent editing current admin/read token
|
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot edit the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot edit the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//TODO
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
Toaster.info("Not implemented", "Currently not implemented");
|
builder: (context) => EditKeyTokenPermissionsDialog(
|
||||||
|
keytoken: keytokenPreview!,
|
||||||
|
onUpdatePermissions: _updatePermissions,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editChannels() {
|
void _editChannels() async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
//TODO prevent editing current admin/read token
|
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot edit the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot edit the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//TODO
|
var ownChannels = (await _futureOwnedChannels.future);
|
||||||
|
ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
||||||
|
|
||||||
Toaster.info("Not implemented", "Currently not implemented");
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => EditKeyTokenChannelsDialog(
|
||||||
|
ownedChannels: ownChannels,
|
||||||
|
keytoken: keytokenPreview!,
|
||||||
|
onUpdateChannels: _updateChannelsSelected,
|
||||||
|
onUpdateSetAllChannels: _updateChannelsAll,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteKey() {
|
void _deleteKey() async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
//TODO prevent deleting current admin/read token
|
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot delete the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) {
|
||||||
|
Toaster.error("Error", "You cannot delete the currently used token");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//TODO
|
try {
|
||||||
|
final r = await UIDialogs.showConfirmDialog(context, 'Really (permanently) delete this Key?', okText: 'Unsubscribe', cancelText: 'Cancel');
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
Toaster.info("Not implemented", "Currently not implemented");
|
await APIClient.deleteKeyToken(acc, keytokenPreview!.keytokenID);
|
||||||
|
widget.needsReload?.call();
|
||||||
|
|
||||||
|
Toaster.info('Logout', 'Successfully deleted the key');
|
||||||
|
|
||||||
|
Navi.pop(context);
|
||||||
|
} catch (exc, trace) {
|
||||||
|
Toaster.error("Error", 'Failed to delete key');
|
||||||
|
ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateChannelsSelected(Set<String> selectedEntries) async {
|
||||||
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: selectedEntries.toList(), allChannels: false);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
keytoken = r;
|
||||||
|
keytokenPreview = r.toPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
Toaster.info("Success", "Key updated");
|
||||||
|
|
||||||
|
widget.needsReload?.call();
|
||||||
|
} catch (exc, trace) {
|
||||||
|
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||||
|
Toaster.error("Error", 'Failed to update key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateChannelsAll() async {
|
||||||
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: [], allChannels: true);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
keytoken = r;
|
||||||
|
keytokenPreview = r.toPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
Toaster.info("Success", "Key updated");
|
||||||
|
|
||||||
|
widget.needsReload?.call();
|
||||||
|
} catch (exc, trace) {
|
||||||
|
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||||
|
Toaster.error("Error", 'Failed to update key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePermissions(String perm) async {
|
||||||
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final r = await APIClient.updateKeyToken(acc, widget.keytokenID, permissions: perm);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
keytoken = r;
|
||||||
|
keytokenPreview = r.toPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
Toaster.info("Success", "Key updated");
|
||||||
|
|
||||||
|
widget.needsReload?.call();
|
||||||
|
} catch (exc, trace) {
|
||||||
|
ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace);
|
||||||
|
Toaster.error("Error", 'Failed to update key');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart';
|
|||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||||
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
@ -35,6 +36,7 @@ class MessageViewPage extends StatefulWidget {
|
|||||||
class _MessageViewPageState extends State<MessageViewPage> {
|
class _MessageViewPageState extends State<MessageViewPage> {
|
||||||
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
||||||
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
||||||
|
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
||||||
|
|
||||||
final ScrollController _controller = ScrollController();
|
final ScrollController _controller = ScrollController();
|
||||||
@ -164,8 +166,12 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
icon: FontAwesomeIcons.solidGearCode,
|
icon: FontAwesomeIcons.solidGearCode,
|
||||||
title: 'KeyToken',
|
title: 'KeyToken',
|
||||||
values: [message.usedKeyID, token?.name ?? '...'],
|
values: [message.usedKeyID, token?.name ?? '...'],
|
||||||
mainAction: () => {
|
mainAction: () {
|
||||||
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])))
|
if (message.senderUserID == userAccUserID) {
|
||||||
|
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
|
||||||
|
} else {
|
||||||
|
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
UI.metaCard(
|
UI.metaCard(
|
||||||
@ -210,7 +216,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var showScrollbar = true;
|
var showScrollbar = false;
|
||||||
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
||||||
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
||||||
|
|
||||||
|
@ -42,9 +42,9 @@ enum SubscriptionViewPageInitState { loading, okay, error }
|
|||||||
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
|
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
||||||
|
|
||||||
late ImmediateFuture<UserPreview> _futureChannelOwner;
|
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<UserPreview> _futureSubscriber;
|
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
|
||||||
late ImmediateFuture<ChannelPreview> _futureChannel;
|
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
|
||||||
|
|
||||||
int _loadingIndeterminateCounter = 0;
|
int _loadingIndeterminateCounter = 0;
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||||
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
import 'package:simplecloudnotifier/settings/app_settings.dart';
|
||||||
|
|
||||||
@ -57,4 +59,21 @@ class SCNDataCache {
|
|||||||
|
|
||||||
return cacheMessages;
|
return cacheMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<KeyToken> getOrQueryTokenByValue(String uid, String tokVal) async {
|
||||||
|
final cache = Hive.box<KeyToken>('scn-keytoken-value-cache');
|
||||||
|
|
||||||
|
final cacheVal = cache.get(tokVal);
|
||||||
|
if (cacheVal != null) {
|
||||||
|
print('[SCNDataCache] Found Token(${tokVal}) in cache');
|
||||||
|
return Future.value(cacheVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
final tok = await APIClient.getKeyTokenByToken(uid, tokVal);
|
||||||
|
|
||||||
|
print('[SCNDataCache] Queried Token(${tokVal}) from API');
|
||||||
|
await cache.put(tokVal, tok);
|
||||||
|
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting
|
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting
|
||||||
// This way we can set the ImmediateFuture.value directly and circumvent that.
|
// This way we can set the ImmediateFuture.value directly and circumvent that.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
class ImmediateFuture<T> {
|
class ImmediateFuture<T> {
|
||||||
final Future<T> future;
|
final Future<T> future;
|
||||||
final T? value;
|
final T? value;
|
||||||
@ -20,6 +22,10 @@ class ImmediateFuture<T> {
|
|||||||
: future = Future.value(v),
|
: future = Future.value(v),
|
||||||
value = v;
|
value = v;
|
||||||
|
|
||||||
|
ImmediateFuture.ofPending()
|
||||||
|
: future = Completer<T>().future,
|
||||||
|
value = null;
|
||||||
|
|
||||||
T? get() {
|
T? get() {
|
||||||
return value ?? _futureValue;
|
return value ?? _futureValue;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ func (h APIHandler) GetCurrentUserKey(pctx ginext.PreContext) ginext.HTTPRespons
|
|||||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keyToken", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, keytoken, "INCLUDE_TOKEN"))
|
return finishSuccess(ginext.JSONWithFilter(http.StatusOK, keytoken, "INCLUDE_TOKEN"))
|
||||||
@ -153,7 +153,7 @@ func (h APIHandler) GetUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return finishSuccess(ginext.JSON(http.StatusOK, keytoken))
|
return finishSuccess(ginext.JSON(http.StatusOK, keytoken))
|
||||||
@ -210,7 +210,7 @@ func (h APIHandler) UpdateUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.Name != nil {
|
if b.Name != nil {
|
||||||
@ -372,12 +372,12 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
token, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.KeyID == *ctx.GetPermissionKeyTokenID() {
|
if u.KeyID == *ctx.GetPermissionKeyTokenID() {
|
||||||
@ -386,10 +386,10 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
|
|||||||
|
|
||||||
err = h.database.DeleteKeyToken(ctx, u.KeyID)
|
err = h.database.DeleteKeyToken(ctx, u.KeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err)
|
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete key", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return finishSuccess(ginext.JSON(http.StatusOK, client))
|
return finishSuccess(ginext.JSON(http.StatusOK, token))
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user