✨ Bot key management
This commit is contained in:
@@ -891,5 +891,18 @@
|
|||||||
"orderByPopularity": "Sort by popularity",
|
"orderByPopularity": "Sort by popularity",
|
||||||
"orderByReleaseDate": "Sort by release date",
|
"orderByReleaseDate": "Sort by release date",
|
||||||
"editBot": "Edit Bot",
|
"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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/screens/about.dart';
|
import 'package:island/screens/about.dart';
|
||||||
import 'package:island/screens/account/credits.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_app.dart';
|
||||||
import 'package:island/screens/developers/edit_bot.dart';
|
import 'package:island/screens/developers/edit_bot.dart';
|
||||||
import 'package:island/screens/developers/new_app.dart';
|
import 'package:island/screens/developers/new_app.dart';
|
||||||
import 'package:island/screens/developers/hub.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/projects.dart';
|
||||||
import 'package:island/screens/developers/edit_project.dart';
|
import 'package:island/screens/developers/edit_project.dart';
|
||||||
import 'package:island/screens/developers/new_project.dart';
|
import 'package:island/screens/developers/new_project.dart';
|
||||||
@@ -347,11 +349,21 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
id: state.pathParameters['id']!,
|
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(
|
GoRoute(
|
||||||
name: 'developerBotNew',
|
name: 'developerBotNew',
|
||||||
path: 'bots/new',
|
path: 'bots/new',
|
||||||
builder:
|
builder:
|
||||||
(context, state) => EditBotScreen(
|
(context, state) => NewBotScreen(
|
||||||
publisherName: state.pathParameters['name']!,
|
publisherName: state.pathParameters['name']!,
|
||||||
projectId: state.pathParameters['projectId']!,
|
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: () {
|
onTap: () {
|
||||||
context.goNamed(
|
context.pushNamed(
|
||||||
'accountProfile',
|
'developerBotDetail',
|
||||||
pathParameters: {'name': bot.account.name},
|
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