✨ Bot key management
This commit is contained in:
@@ -891,5 +891,18 @@
|
||||
"orderByPopularity": "Sort by popularity",
|
||||
"orderByReleaseDate": "Sort by release date",
|
||||
"editBot": "Edit Bot",
|
||||
"botAutomatedBy": "Automated by {}"
|
||||
"botAutomatedBy": "Automated by {}",
|
||||
"botDetails": "Bot Details",
|
||||
"overview": "Overview",
|
||||
"keys": "Keys",
|
||||
"botNotFound": "Bot not found.",
|
||||
"newBotKey": "New Bot Key",
|
||||
"newBotKeyHint": "Enter a name for your new key. The key will be shown only once.",
|
||||
"revokeBotKey": "Revoke Bot Key",
|
||||
"revokeBotKeyHint": "Are you sure you want to revoke this key? This action cannot be undone and any application using this key will stop working.",
|
||||
"noBotKeys": "No bot keys yet.",
|
||||
"revoke": "Revoke",
|
||||
"keyName": "Key Name",
|
||||
"newKeyGenerated": "New Key Generated",
|
||||
"copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again."
|
||||
}
|
20
lib/models/bot_key.dart
Normal file
20
lib/models/bot_key.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'bot_key.freezed.dart';
|
||||
part 'bot_key.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnAccountApiKey with _$SnAccountApiKey {
|
||||
const factory SnAccountApiKey({
|
||||
required String id,
|
||||
required String label,
|
||||
required String accountId,
|
||||
required String sessionId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
String? key,
|
||||
}) = _SnAccountApiKey;
|
||||
|
||||
factory SnAccountApiKey.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountApiKeyFromJson(json);
|
||||
}
|
289
lib/models/bot_key.freezed.dart
Normal file
289
lib/models/bot_key.freezed.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'bot_key.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnAccountApiKey {
|
||||
|
||||
String get id; String get label; String get accountId; String get sessionId; DateTime get createdAt; DateTime get updatedAt; String? get key;
|
||||
/// Create a copy of SnAccountApiKey
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountApiKeyCopyWith<SnAccountApiKey> get copyWith => _$SnAccountApiKeyCopyWithImpl<SnAccountApiKey>(this as SnAccountApiKey, _$identity);
|
||||
|
||||
/// Serializes this SnAccountApiKey to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnAccountApiKeyCopyWith<$Res> {
|
||||
factory $SnAccountApiKeyCopyWith(SnAccountApiKey value, $Res Function(SnAccountApiKey) _then) = _$SnAccountApiKeyCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnAccountApiKeyCopyWithImpl<$Res>
|
||||
implements $SnAccountApiKeyCopyWith<$Res> {
|
||||
_$SnAccountApiKeyCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnAccountApiKey _self;
|
||||
final $Res Function(SnAccountApiKey) _then;
|
||||
|
||||
/// Create a copy of SnAccountApiKey
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnAccountApiKey].
|
||||
extension SnAccountApiKeyPatterns on SnAccountApiKey {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnAccountApiKey value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnAccountApiKey value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnAccountApiKey value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey():
|
||||
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountApiKey() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnAccountApiKey implements SnAccountApiKey {
|
||||
const _SnAccountApiKey({required this.id, required this.label, required this.accountId, required this.sessionId, required this.createdAt, required this.updatedAt, this.key});
|
||||
factory _SnAccountApiKey.fromJson(Map<String, dynamic> json) => _$SnAccountApiKeyFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String label;
|
||||
@override final String accountId;
|
||||
@override final String sessionId;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final String? key;
|
||||
|
||||
/// Create a copy of SnAccountApiKey
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnAccountApiKeyCopyWith<_SnAccountApiKey> get copyWith => __$SnAccountApiKeyCopyWithImpl<_SnAccountApiKey>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnAccountApiKeyToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnAccountApiKeyCopyWith<$Res> implements $SnAccountApiKeyCopyWith<$Res> {
|
||||
factory _$SnAccountApiKeyCopyWith(_SnAccountApiKey value, $Res Function(_SnAccountApiKey) _then) = __$SnAccountApiKeyCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnAccountApiKeyCopyWithImpl<$Res>
|
||||
implements _$SnAccountApiKeyCopyWith<$Res> {
|
||||
__$SnAccountApiKeyCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnAccountApiKey _self;
|
||||
final $Res Function(_SnAccountApiKey) _then;
|
||||
|
||||
/// Create a copy of SnAccountApiKey
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) {
|
||||
return _then(_SnAccountApiKey(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,sessionId: null == sessionId ? _self.sessionId : sessionId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
29
lib/models/bot_key.g.dart
Normal file
29
lib/models/bot_key.g.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bot_key.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnAccountApiKey _$SnAccountApiKeyFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountApiKey(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String,
|
||||
accountId: json['account_id'] as String,
|
||||
sessionId: json['session_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
key: json['key'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnAccountApiKeyToJson(_SnAccountApiKey instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'account_id': instance.accountId,
|
||||
'session_id': instance.sessionId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'key': instance.key,
|
||||
};
|
@@ -7,10 +7,12 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/about.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/screens/developers/bot_detail.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/screens/developers/new_bot.dart';
|
||||
import 'package:island/screens/developers/projects.dart';
|
||||
import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
@@ -347,11 +349,21 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotDetail',
|
||||
path: 'bots/:botId',
|
||||
builder:
|
||||
(context, state) => BotDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotNew',
|
||||
path: 'bots/new',
|
||||
builder:
|
||||
(context, state) => EditBotScreen(
|
||||
(context, state) => NewBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
|
142
lib/screens/developers/bot_detail.dart
Normal file
142
lib/screens/developers/bot_detail.dart
Normal file
@@ -0,0 +1,142 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/bot.dart';
|
||||
import 'package:island/screens/developers/bot_keys.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class BotDetailScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String botId;
|
||||
|
||||
const BotDetailScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
required this.botId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final botData = ref.watch(botProvider(publisherName, projectId, botId));
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed:
|
||||
botData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerBotEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': botId,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
tabs: [Tab(text: 'overview'.tr()), Tab(text: 'keys'.tr())],
|
||||
),
|
||||
),
|
||||
body: botData.when(
|
||||
data: (bot) {
|
||||
if (bot == null) {
|
||||
return Center(child: Text('botNotFound'.tr()));
|
||||
}
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_BotOverview(bot: bot),
|
||||
BotKeysScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
botId: botId,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
botProvider(publisherName, projectId, botId),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BotOverview extends StatelessWidget {
|
||||
final Bot bot;
|
||||
const _BotOverview({required this.bot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child:
|
||||
bot.account.profile.background != null
|
||||
? CloudFileWidget(
|
||||
item: bot.account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: bot.account.profile.picture?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
ListTile(title: Text('name'.tr()), subtitle: Text(bot.account.name)),
|
||||
ListTile(
|
||||
title: Text('nickname'.tr()),
|
||||
subtitle: Text(bot.account.nick),
|
||||
),
|
||||
ListTile(title: Text('slug'.tr()), subtitle: Text(bot.slug)),
|
||||
if (bot.account.profile.bio.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text('bio'.tr()),
|
||||
subtitle: Text(bot.account.profile.bio),
|
||||
),
|
||||
],
|
||||
).padding(bottom: 24),
|
||||
);
|
||||
}
|
||||
}
|
237
lib/screens/developers/bot_keys.dart
Normal file
237
lib/screens/developers/bot_keys.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/bot_key.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'bot_keys.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnAccountApiKey>> botKeys(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String botId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
||||
);
|
||||
return (resp.data as List).map((e) => SnAccountApiKey.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
class BotKeysScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String botId;
|
||||
|
||||
const BotKeysScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
required this.botId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final keys = ref.watch(botKeysProvider(publisherName, projectId, botId));
|
||||
final keyNameController = useTextEditingController();
|
||||
|
||||
void showNewKeySheet(SnAccountApiKey newApiKey) {
|
||||
final token = newApiKey.key;
|
||||
if (token == null) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'newKeyGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copyKeyHint'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(token),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: token));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).whenComplete(() {
|
||||
ref.invalidate(botKeysProvider(publisherName, projectId, botId));
|
||||
});
|
||||
}
|
||||
|
||||
void createKey() {
|
||||
keyNameController.clear();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
heightFactor: 0.65,
|
||||
titleText: 'newBotKey'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: keyNameController,
|
||||
decoration: InputDecoration(labelText: 'keyName'.tr()),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
if (keyNameController.text.isEmpty) return;
|
||||
final keyName = keyNameController.text;
|
||||
Navigator.pop(context); // Close the sheet
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
||||
data: {'label': keyName},
|
||||
);
|
||||
final newApiKey = SnAccountApiKey.fromJson(resp.data);
|
||||
showNewKeySheet(newApiKey);
|
||||
} catch (e) {
|
||||
showErrorAlert(e.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('create'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void revokeKey(String keyId) {
|
||||
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
|
||||
confirm,
|
||||
) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client
|
||||
.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId',
|
||||
)
|
||||
.then((_) {
|
||||
ref.invalidate(
|
||||
botKeysProvider(publisherName, projectId, botId),
|
||||
);
|
||||
})
|
||||
.catchError((err) {
|
||||
showErrorAlert(err.toString());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('newBotKey'.tr()),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: createKey,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: keys.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return Center(child: Text('noBotKeys'.tr()));
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
botKeysProvider(publisherName, projectId, botId).future,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final apiKey = data[index];
|
||||
return ListTile(
|
||||
title: Text(apiKey.label),
|
||||
subtitle: Text(
|
||||
'Created: ${DateFormat.yMMMd().format(apiKey.createdAt)}',
|
||||
),
|
||||
contentPadding: EdgeInsets.only(left: 16, right: 12),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'revoke',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'revoke'.tr(),
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'revoke') {
|
||||
revokeKey(apiKey.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
botKeysProvider(publisherName, projectId, botId),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
172
lib/screens/developers/bot_keys.g.dart
Normal file
172
lib/screens/developers/bot_keys.g.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bot_keys.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$botKeysHash() => r'f7d1121833dc3da0cbd84b6171c2b2539edeb785';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [botKeys].
|
||||
@ProviderFor(botKeys)
|
||||
const botKeysProvider = BotKeysFamily();
|
||||
|
||||
/// See also [botKeys].
|
||||
class BotKeysFamily extends Family<AsyncValue<List<SnAccountApiKey>>> {
|
||||
/// See also [botKeys].
|
||||
const BotKeysFamily();
|
||||
|
||||
/// See also [botKeys].
|
||||
BotKeysProvider call(String publisherName, String projectId, String botId) {
|
||||
return BotKeysProvider(publisherName, projectId, botId);
|
||||
}
|
||||
|
||||
@override
|
||||
BotKeysProvider getProviderOverride(covariant BotKeysProvider provider) {
|
||||
return call(provider.publisherName, provider.projectId, provider.botId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'botKeysProvider';
|
||||
}
|
||||
|
||||
/// See also [botKeys].
|
||||
class BotKeysProvider extends AutoDisposeFutureProvider<List<SnAccountApiKey>> {
|
||||
/// See also [botKeys].
|
||||
BotKeysProvider(String publisherName, String projectId, String botId)
|
||||
: this._internal(
|
||||
(ref) => botKeys(ref as BotKeysRef, publisherName, projectId, botId),
|
||||
from: botKeysProvider,
|
||||
name: r'botKeysProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$botKeysHash,
|
||||
dependencies: BotKeysFamily._dependencies,
|
||||
allTransitiveDependencies: BotKeysFamily._allTransitiveDependencies,
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
botId: botId,
|
||||
);
|
||||
|
||||
BotKeysProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
required this.botId,
|
||||
}) : super.internal();
|
||||
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String botId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<SnAccountApiKey>> Function(BotKeysRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: BotKeysProvider._internal(
|
||||
(ref) => create(ref as BotKeysRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
botId: botId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<SnAccountApiKey>> createElement() {
|
||||
return _BotKeysProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BotKeysProvider &&
|
||||
other.publisherName == publisherName &&
|
||||
other.projectId == projectId &&
|
||||
other.botId == botId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, publisherName.hashCode);
|
||||
hash = _SystemHash.combine(hash, projectId.hashCode);
|
||||
hash = _SystemHash.combine(hash, botId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin BotKeysRef on AutoDisposeFutureProviderRef<List<SnAccountApiKey>> {
|
||||
/// The parameter `publisherName` of this provider.
|
||||
String get publisherName;
|
||||
|
||||
/// The parameter `projectId` of this provider.
|
||||
String get projectId;
|
||||
|
||||
/// The parameter `botId` of this provider.
|
||||
String get botId;
|
||||
}
|
||||
|
||||
class _BotKeysProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<SnAccountApiKey>>
|
||||
with BotKeysRef {
|
||||
_BotKeysProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get publisherName => (origin as BotKeysProvider).publisherName;
|
||||
@override
|
||||
String get projectId => (origin as BotKeysProvider).projectId;
|
||||
@override
|
||||
String get botId => (origin as BotKeysProvider).botId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -140,9 +140,13 @@ class BotsScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
context.goNamed(
|
||||
'accountProfile',
|
||||
pathParameters: {'name': bot.account.name},
|
||||
context.pushNamed(
|
||||
'developerBotDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'botId': bot.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
14
lib/screens/developers/new_bot.dart
Normal file
14
lib/screens/developers/new_bot.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
|
||||
class NewBotScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
const NewBotScreen({super.key, required this.publisherName, required this.projectId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditBotScreen(publisherName: publisherName, projectId: projectId);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user