diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index d5c0ecc1..f3ffb17c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -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." +} \ No newline at end of file diff --git a/lib/models/bot_key.dart b/lib/models/bot_key.dart new file mode 100644 index 00000000..ede13527 --- /dev/null +++ b/lib/models/bot_key.dart @@ -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 json) => + _$SnAccountApiKeyFromJson(json); +} diff --git a/lib/models/bot_key.freezed.dart b/lib/models/bot_key.freezed.dart new file mode 100644 index 00000000..10accec6 --- /dev/null +++ b/lib/models/bot_key.freezed.dart @@ -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 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 get copyWith => _$SnAccountApiKeyCopyWithImpl(this as SnAccountApiKey, _$identity); + + /// Serializes this SnAccountApiKey to a JSON map. + Map 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 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 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? 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 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 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? 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 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 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 diff --git a/lib/models/bot_key.g.dart b/lib/models/bot_key.g.dart new file mode 100644 index 00000000..6d434a2c --- /dev/null +++ b/lib/models/bot_key.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bot_key.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SnAccountApiKey _$SnAccountApiKeyFromJson(Map 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 _$SnAccountApiKeyToJson(_SnAccountApiKey instance) => + { + '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, + }; diff --git a/lib/route.dart b/lib/route.dart index 0927a8d2..d6135f52 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -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((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']!, ), diff --git a/lib/screens/developers/bot_detail.dart b/lib/screens/developers/bot_detail.dart new file mode 100644 index 00000000..a04dbb9e --- /dev/null +++ b/lib/screens/developers/bot_detail.dart @@ -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), + ); + } +} diff --git a/lib/screens/developers/bot_keys.dart b/lib/screens/developers/bot_keys.dart new file mode 100644 index 00000000..15640332 --- /dev/null +++ b/lib/screens/developers/bot_keys.dart @@ -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> 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), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/developers/bot_keys.g.dart b/lib/screens/developers/bot_keys.g.dart new file mode 100644 index 00000000..7fde16a8 --- /dev/null +++ b/lib/screens/developers/bot_keys.g.dart @@ -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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'botKeysProvider'; +} + +/// See also [botKeys]. +class BotKeysProvider extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// 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> + 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 diff --git a/lib/screens/developers/bots.dart b/lib/screens/developers/bots.dart index 2fff4bbc..5c995317 100644 --- a/lib/screens/developers/bots.dart +++ b/lib/screens/developers/bots.dart @@ -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, + }, ); }, ), diff --git a/lib/screens/developers/new_bot.dart b/lib/screens/developers/new_bot.dart new file mode 100644 index 00000000..9d93e080 --- /dev/null +++ b/lib/screens/developers/new_bot.dart @@ -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); + } +} diff --git a/swagger-develop.json b/swagger.json similarity index 100% rename from swagger-develop.json rename to swagger.json