🎨 Use feature based folder structure
This commit is contained in:
168
lib/developers/developers/app_detail.dart
Normal file
168
lib/developers/developers/app_detail.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/developers/developers/app_secrets.dart';
|
||||
import 'package:island/developers/developers/apps.dart';
|
||||
import 'package:island/developers/developers_models/custom_app.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class AppDetailScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String appId;
|
||||
|
||||
const AppDetailScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
required this.appId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final appData = ref.watch(
|
||||
customAppProvider(publisherName, projectId, appId),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(appData.value?.name ?? 'appDetails'.tr()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed: appData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerAppEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': appId,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'overview'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'secrets'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: appData.when(
|
||||
data: (app) {
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: _AppOverview(app: app),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: AppSecretsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
appId: appId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppOverview extends StatelessWidget {
|
||||
final CustomApp app;
|
||||
const _AppOverview({required this.app});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: app.background != null
|
||||
? CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: ProfilePictureWidget(
|
||||
file: app.picture,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
ListTile(title: Text('name'.tr()), subtitle: Text(app.name)),
|
||||
ListTile(title: Text('slug'.tr()), subtitle: Text(app.slug)),
|
||||
if (app.description?.isNotEmpty ?? false)
|
||||
ListTile(
|
||||
title: Text('description'.tr()),
|
||||
subtitle: Text(app.description!),
|
||||
),
|
||||
],
|
||||
).padding(bottom: 24),
|
||||
);
|
||||
}
|
||||
}
|
||||
262
lib/developers/developers/app_secrets.dart
Normal file
262
lib/developers/developers/app_secrets.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
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/developers/developers_models/custom_app_secret.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/core/services/time.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'app_secrets.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<CustomAppSecret>> customAppSecrets(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String appId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets',
|
||||
);
|
||||
return (resp.data as List)
|
||||
.map((e) => CustomAppSecret.fromJson(e))
|
||||
.cast<CustomAppSecret>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class AppSecretsScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String appId;
|
||||
|
||||
const AppSecretsScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
required this.appId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final secrets = ref.watch(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
);
|
||||
|
||||
void showNewSecretSheet(String newSecret) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'newSecretGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copySecretHint'.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(newSecret),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: newSecret));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).whenComplete(() {
|
||||
ref.invalidate(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void createSecret() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return HookBuilder(
|
||||
builder: (context) {
|
||||
final descriptionController = useTextEditingController();
|
||||
final expiresInController = useTextEditingController();
|
||||
final isOidc = useState(false);
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'generateSecret'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
const Gap(16),
|
||||
TextFormField(
|
||||
controller: expiresInController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'expiresIn'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const Gap(16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SwitchListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text('isOidc'.tr()),
|
||||
value: isOidc.value,
|
||||
onChanged: (value) => isOidc.value = value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
final description = descriptionController.text;
|
||||
final expiresIn = int.tryParse(
|
||||
expiresInController.text,
|
||||
);
|
||||
Navigator.pop(context); // Close the sheet
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets',
|
||||
data: {
|
||||
'description': description,
|
||||
'expires_in': expiresIn,
|
||||
'is_oidc': isOidc.value,
|
||||
},
|
||||
);
|
||||
final newSecret = CustomAppSecret.fromJson(
|
||||
resp.data,
|
||||
);
|
||||
if (newSecret.secret != null) {
|
||||
showNewSecretSheet(newSecret.secret!);
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('create'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return secrets.when(
|
||||
data: (data) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(
|
||||
customAppSecretsProvider(publisherName, projectId, appId).future,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('generateSecret'.tr()),
|
||||
onTap: createSecret,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final secret = data[index];
|
||||
return ListTile(
|
||||
title: Text(secret.description ?? secret.id),
|
||||
subtitle: Text(
|
||||
'createdAt'.tr(args: [secret.createdAt.formatSystem()]),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete, color: Colors.red),
|
||||
onPressed: () {
|
||||
showConfirmAlert(
|
||||
'deleteSecretHint'.tr(),
|
||||
'deleteSecret'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
customAppSecretsProvider(
|
||||
publisherName,
|
||||
projectId,
|
||||
appId,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/developers/developers/app_secrets.g.dart
Normal file
97
lib/developers/developers/app_secrets.g.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_secrets.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(customAppSecrets)
|
||||
final customAppSecretsProvider = CustomAppSecretsFamily._();
|
||||
|
||||
final class CustomAppSecretsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<CustomAppSecret>>,
|
||||
List<CustomAppSecret>,
|
||||
FutureOr<List<CustomAppSecret>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<CustomAppSecret>>,
|
||||
$FutureProvider<List<CustomAppSecret>> {
|
||||
CustomAppSecretsProvider._({
|
||||
required CustomAppSecretsFamily super.from,
|
||||
required (String, String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'customAppSecretsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customAppSecretsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'customAppSecretsProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<CustomAppSecret>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<CustomAppSecret>> create(Ref ref) {
|
||||
final argument = this.argument as (String, String, String);
|
||||
return customAppSecrets(ref, argument.$1, argument.$2, argument.$3);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CustomAppSecretsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$customAppSecretsHash() => r'1bc62ad812487883ce739793b22a76168d656752';
|
||||
|
||||
final class CustomAppSecretsFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<List<CustomAppSecret>>,
|
||||
(String, String, String)
|
||||
> {
|
||||
CustomAppSecretsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'customAppSecretsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
CustomAppSecretsProvider call(
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String appId,
|
||||
) => CustomAppSecretsProvider._(
|
||||
argument: (publisherName, projectId, appId),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'customAppSecretsProvider';
|
||||
}
|
||||
255
lib/developers/developers/apps.dart
Normal file
255
lib/developers/developers/apps.dart
Normal file
@@ -0,0 +1,255 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/developers/developers/edit_app.dart';
|
||||
import 'package:island/developers/developers/new_app.dart';
|
||||
import 'package:island/developers/developers_models/custom_app.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'apps.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<CustomApp> customApp(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String appId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$appId',
|
||||
);
|
||||
return CustomApp.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<CustomApp>> customApps(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps',
|
||||
);
|
||||
return (resp.data as List)
|
||||
.map((e) => CustomApp.fromJson(e))
|
||||
.cast<CustomApp>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class CustomAppsScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
const CustomAppsScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final apps = ref.watch(customAppsProvider(publisherName, projectId));
|
||||
|
||||
return apps.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('noCustomApps').tr(),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('createCustomApp').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh: () =>
|
||||
ref.refresh(customAppsProvider(publisherName, projectId).future),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text('customApps').tr().padding(horizontal: 8),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final app = data[index];
|
||||
return Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'developerAppDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'appId': app.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
if (app.background != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(app.name),
|
||||
leading: ProfilePictureWidget(
|
||||
file: app.picture,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
subtitle: Text(
|
||||
app.slug,
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 12,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editCustomApp'.tr(),
|
||||
child: EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: app.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteCustomAppHint'.tr(),
|
||||
'deleteCustomApp'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(
|
||||
apiClientProvider,
|
||||
);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
customAppsProvider(
|
||||
publisherName,
|
||||
projectId,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () =>
|
||||
ref.invalidate(customAppsProvider(publisherName, projectId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
170
lib/developers/developers/apps.g.dart
Normal file
170
lib/developers/developers/apps.g.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'apps.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(customApp)
|
||||
final customAppProvider = CustomAppFamily._();
|
||||
|
||||
final class CustomAppProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CustomApp>,
|
||||
CustomApp,
|
||||
FutureOr<CustomApp>
|
||||
>
|
||||
with $FutureModifier<CustomApp>, $FutureProvider<CustomApp> {
|
||||
CustomAppProvider._({
|
||||
required CustomAppFamily super.from,
|
||||
required (String, String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'customAppProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customAppHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'customAppProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<CustomApp> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<CustomApp> create(Ref ref) {
|
||||
final argument = this.argument as (String, String, String);
|
||||
return customApp(ref, argument.$1, argument.$2, argument.$3);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CustomAppProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$customAppHash() => r'be05431ba8bf06fd20ee988a61c3663a68e15fc9';
|
||||
|
||||
final class CustomAppFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<CustomApp>,
|
||||
(String, String, String)
|
||||
> {
|
||||
CustomAppFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'customAppProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
CustomAppProvider call(
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String appId,
|
||||
) => CustomAppProvider._(
|
||||
argument: (publisherName, projectId, appId),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'customAppProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(customApps)
|
||||
final customAppsProvider = CustomAppsFamily._();
|
||||
|
||||
final class CustomAppsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<CustomApp>>,
|
||||
List<CustomApp>,
|
||||
FutureOr<List<CustomApp>>
|
||||
>
|
||||
with $FutureModifier<List<CustomApp>>, $FutureProvider<List<CustomApp>> {
|
||||
CustomAppsProvider._({
|
||||
required CustomAppsFamily super.from,
|
||||
required (String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'customAppsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customAppsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'customAppsProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<CustomApp>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<CustomApp>> create(Ref ref) {
|
||||
final argument = this.argument as (String, String);
|
||||
return customApps(ref, argument.$1, argument.$2);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CustomAppsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$customAppsHash() => r'450bedaf4220b8963cb44afeb14d4c0e80f01b11';
|
||||
|
||||
final class CustomAppsFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<FutureOr<List<CustomApp>>, (String, String)> {
|
||||
CustomAppsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'customAppsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
CustomAppsProvider call(String publisherName, String projectId) =>
|
||||
CustomAppsProvider._(argument: (publisherName, projectId), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'customAppsProvider';
|
||||
}
|
||||
169
lib/developers/developers/bot_detail.dart
Normal file
169
lib/developers/developers/bot_detail.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
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/developers/developers/bot_keys.dart';
|
||||
import 'package:island/developers/developers/edit_bot.dart';
|
||||
import 'package:island/developers/developers_models/bot.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/shared/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(
|
||||
isNoBackground: false,
|
||||
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(
|
||||
child: Text(
|
||||
'overview'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'keys'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: botData.when(
|
||||
data: (bot) {
|
||||
if (bot == null) {
|
||||
return Center(child: Text('botNotFound'.tr()));
|
||||
}
|
||||
return TabBarView(
|
||||
controller: tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: _BotOverview(bot: bot),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: 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(
|
||||
file: bot.account.profile.picture,
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
271
lib/developers/developers/bot_keys.dart
Normal file
271
lib/developers/developers/bot_keys.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
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/developers/developers_models/bot_key.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/core/services/time.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/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.7,
|
||||
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(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
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 rotateKey(String keyId) {
|
||||
showConfirmAlert('rotateBotKeyHint'.tr(), 'rotateBotKey'.tr()).then((
|
||||
confirm,
|
||||
) async {
|
||||
if (confirm) {
|
||||
try {
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId/rotate',
|
||||
);
|
||||
final rotatedApiKey = SnAccountApiKey.fromJson(resp.data);
|
||||
showNewKeySheet(rotatedApiKey);
|
||||
} catch (err) {
|
||||
showErrorAlert(err.toString());
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void revokeKey(String keyId) {
|
||||
showConfirmAlert(
|
||||
'revokeBotKeyHint'.tr(),
|
||||
'revokeBotKey'.tr(),
|
||||
isDanger: true,
|
||||
).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 keys.when(
|
||||
data: (data) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('newBotKey'.tr()),
|
||||
onTap: createKey,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: data.isEmpty
|
||||
? Center(child: Text('noBotKeys'.tr()))
|
||||
: 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(apiKey.createdAt.formatSystem()),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 12,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'rotate',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.refresh),
|
||||
const Gap(12),
|
||||
Text('rotateKey'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 == 'rotate') {
|
||||
rotateKey(apiKey.id);
|
||||
} else if (value == 'revoke') {
|
||||
revokeKey(apiKey.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () =>
|
||||
ref.invalidate(botKeysProvider(publisherName, projectId, botId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/developers/developers/bot_keys.g.dart
Normal file
94
lib/developers/developers/bot_keys.g.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bot_keys.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(botKeys)
|
||||
final botKeysProvider = BotKeysFamily._();
|
||||
|
||||
final class BotKeysProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnAccountApiKey>>,
|
||||
List<SnAccountApiKey>,
|
||||
FutureOr<List<SnAccountApiKey>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SnAccountApiKey>>,
|
||||
$FutureProvider<List<SnAccountApiKey>> {
|
||||
BotKeysProvider._({
|
||||
required BotKeysFamily super.from,
|
||||
required (String, String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'botKeysProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$botKeysHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'botKeysProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnAccountApiKey>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnAccountApiKey>> create(Ref ref) {
|
||||
final argument = this.argument as (String, String, String);
|
||||
return botKeys(ref, argument.$1, argument.$2, argument.$3);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BotKeysProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$botKeysHash() => r'f7d1121833dc3da0cbd84b6171c2b2539edeb785';
|
||||
|
||||
final class BotKeysFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<List<SnAccountApiKey>>,
|
||||
(String, String, String)
|
||||
> {
|
||||
BotKeysFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'botKeysProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BotKeysProvider call(String publisherName, String projectId, String botId) =>
|
||||
BotKeysProvider._(
|
||||
argument: (publisherName, projectId, botId),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'botKeysProvider';
|
||||
}
|
||||
224
lib/developers/developers/bots.dart
Normal file
224
lib/developers/developers/bots.dart
Normal file
@@ -0,0 +1,224 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/developers/developers/edit_bot.dart';
|
||||
import 'package:island/developers/developers/new_bot.dart';
|
||||
import 'package:island/developers/developers_models/bot.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'bots.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<Bot>> bots(Ref ref, String publisherName, String projectId) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots',
|
||||
);
|
||||
return (resp.data as List).map((e) => Bot.fromJson(e)).cast<Bot>().toList();
|
||||
}
|
||||
|
||||
class BotsScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
const BotsScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final botsList = ref.watch(botsProvider(publisherName, projectId));
|
||||
|
||||
return botsList.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('noBots').tr(),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('createBot').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh: () =>
|
||||
ref.refresh(botsProvider(publisherName, projectId).future),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: Text('bots').tr().padding(horizontal: 8),
|
||||
trailing: IconButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final bot = data[index];
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
if (bot.account.profile.background != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: CloudFileWidget(
|
||||
item: bot.account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
),
|
||||
ListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0),
|
||||
),
|
||||
),
|
||||
leading: ProfilePictureWidget(
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
file: bot.account.profile.picture,
|
||||
),
|
||||
title: Text(bot.account.nick),
|
||||
subtitle: Text(bot.account.name),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editBot'.tr(),
|
||||
child: EditBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: bot.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteBotHint'.tr(),
|
||||
'deleteBot'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(
|
||||
apiClientProvider,
|
||||
);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
botsProvider(publisherName, projectId),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'developerBotDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'botId': bot.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(botsProvider(publisherName, projectId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
lib/developers/developers/bots.g.dart
Normal file
84
lib/developers/developers/bots.g.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bots.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(bots)
|
||||
final botsProvider = BotsFamily._();
|
||||
|
||||
final class BotsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Bot>>,
|
||||
List<Bot>,
|
||||
FutureOr<List<Bot>>
|
||||
>
|
||||
with $FutureModifier<List<Bot>>, $FutureProvider<List<Bot>> {
|
||||
BotsProvider._({
|
||||
required BotsFamily super.from,
|
||||
required (String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'botsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$botsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'botsProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Bot>> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Bot>> create(Ref ref) {
|
||||
final argument = this.argument as (String, String);
|
||||
return bots(ref, argument.$1, argument.$2);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BotsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$botsHash() => r'15cefd5781350eb68208a342e85fcb0b9e0e3269';
|
||||
|
||||
final class BotsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<Bot>>, (String, String)> {
|
||||
BotsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'botsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BotsProvider call(String publisherName, String projectId) =>
|
||||
BotsProvider._(argument: (publisherName, projectId), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'botsProvider';
|
||||
}
|
||||
595
lib/developers/developers/edit_app.dart
Normal file
595
lib/developers/developers/edit_app.dart
Normal file
@@ -0,0 +1,595 @@
|
||||
import 'package:croppy/croppy.dart' hide cropImage;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/core/services/image.dart';
|
||||
import 'package:island/developers/developers/apps.dart';
|
||||
import 'package:island/developers/developers_models/custom_app.dart';
|
||||
import 'package:island/drive/drive_models/file.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/drive/drive_service.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
|
||||
part 'edit_app.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<CustomApp?> customApp(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String id,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
|
||||
);
|
||||
return CustomApp.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class EditAppScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String? id;
|
||||
final bool isModal;
|
||||
const EditAppScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.id,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isNew = id == null;
|
||||
final app = isNew
|
||||
? null
|
||||
: ref.watch(customAppProvider(publisherName, projectId, id!));
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final slugController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
|
||||
final enableLinks = useState(false); // Only for UI purposes
|
||||
final homePageController = useTextEditingController();
|
||||
final privacyPolicyController = useTextEditingController();
|
||||
final termsController = useTextEditingController();
|
||||
final oauthEnabled = useState(false);
|
||||
final redirectUris = useState<List<String>>([]);
|
||||
final postLogoutUris = useState<List<String>>([]);
|
||||
final allowedScopes = useState<List<String>>([
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
]);
|
||||
final allowedGrantTypes = useState<List<String>>([
|
||||
'authorization_code',
|
||||
'refresh_token',
|
||||
]);
|
||||
final requirePkce = useState(true);
|
||||
final allowOfflineAccess = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
if (app?.value != null) {
|
||||
nameController.text = app!.value!.name;
|
||||
slugController.text = app.value!.slug;
|
||||
descriptionController.text = app.value!.description ?? '';
|
||||
picture.value = app.value!.picture;
|
||||
background.value = app.value!.background;
|
||||
homePageController.text = app.value!.links?.homePage ?? '';
|
||||
privacyPolicyController.text = app.value!.links?.privacyPolicy ?? '';
|
||||
termsController.text = app.value!.links?.termsOfService ?? '';
|
||||
if (app.value!.oauthConfig != null) {
|
||||
oauthEnabled.value = true;
|
||||
redirectUris.value = app.value!.oauthConfig!.redirectUris;
|
||||
postLogoutUris.value =
|
||||
app.value!.oauthConfig!.postLogoutRedirectUris ?? [];
|
||||
allowedScopes.value = app.value!.oauthConfig!.allowedScopes;
|
||||
allowedGrantTypes.value = app.value!.oauthConfig!.allowedGrantTypes;
|
||||
requirePkce.value = app.value!.oauthConfig!.requirePkce;
|
||||
allowOfflineAccess.value = app.value!.oauthConfig!.allowOfflineAccess;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [app]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void showAddScopeDialog() {
|
||||
final scopeController = TextEditingController();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'addScope'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: scopeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'scopeName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (scopeController.text.isNotEmpty) {
|
||||
allowedScopes.value = [
|
||||
...allowedScopes.value,
|
||||
scopeController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showAddRedirectUriDialog() {
|
||||
final uriController = TextEditingController();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'addRedirectUri'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'redirectUri'.tr(),
|
||||
hintText: 'https://example.com/auth/callback',
|
||||
helperText: 'redirectUriHint'.tr(),
|
||||
helperMaxLines: 3,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'uriRequired'.tr();
|
||||
}
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
return 'invalidUri'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
if (uriController.text.isNotEmpty) {
|
||||
redirectUris.value = [
|
||||
...redirectUris.value,
|
||||
uriController.text,
|
||||
];
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('add').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void performAction() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final data = {
|
||||
'name': nameController.text,
|
||||
'slug': slugController.text,
|
||||
'description': descriptionController.text,
|
||||
'picture_id': picture.value?.id,
|
||||
'background_id': background.value?.id,
|
||||
'links': {
|
||||
'home_page': homePageController.text.isNotEmpty
|
||||
? homePageController.text
|
||||
: null,
|
||||
'privacy_policy': privacyPolicyController.text.isNotEmpty
|
||||
? privacyPolicyController.text
|
||||
: null,
|
||||
'terms_of_service': termsController.text.isNotEmpty
|
||||
? termsController.text
|
||||
: null,
|
||||
},
|
||||
'oauth_config': oauthEnabled.value
|
||||
? {
|
||||
'redirect_uris': redirectUris.value,
|
||||
'post_logout_redirect_uris': postLogoutUris.value.isNotEmpty
|
||||
? postLogoutUris.value
|
||||
: null,
|
||||
'allowed_scopes': allowedScopes.value,
|
||||
'allowed_grant_types': allowedGrantTypes.value,
|
||||
'require_pkce': requirePkce.value,
|
||||
'allow_offline_access': allowOfflineAccess.value,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
if (isNew) {
|
||||
await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps',
|
||||
data: data,
|
||||
);
|
||||
} else {
|
||||
await client.patch(
|
||||
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
return;
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
ref.invalidate(customAppsProvider(publisherName, projectId));
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
final bodyContent = app == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: app?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: app!.error,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, id!),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: picture.value,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ExpansionPanelList(
|
||||
expansionCallback: (index, isExpanded) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
enableLinks.value = isExpanded;
|
||||
break;
|
||||
case 1:
|
||||
oauthEnabled.value = isExpanded;
|
||||
break;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) =>
|
||||
ListTile(title: Text('appLinks').tr()),
|
||||
body: Column(
|
||||
spacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: homePageController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'homePageUrl'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: privacyPolicyController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'privacyPolicyUrl'.tr(),
|
||||
hintText: 'https://example.com/privacy',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
TextFormField(
|
||||
controller: termsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'termsOfServiceUrl'.tr(),
|
||||
hintText: 'https://example.com/terms',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: enableLinks.value,
|
||||
),
|
||||
ExpansionPanel(
|
||||
headerBuilder: (context, isExpanded) =>
|
||||
ListTile(title: Text('oauthConfig').tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('redirectUris'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...redirectUris.value.map(
|
||||
(uri) => ListTile(
|
||||
title: Text(uri),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
redirectUris.value = redirectUris
|
||||
.value
|
||||
.where((u) => u != uri)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (redirectUris.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('addRedirectUri'.tr()),
|
||||
onTap: showAddRedirectUriDialog,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('allowedScopes'.tr()),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
...allowedScopes.value.map(
|
||||
(scope) => ListTile(
|
||||
title: Text(scope),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
allowedScopes.value =
|
||||
allowedScopes.value
|
||||
.where((s) => s != scope)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (allowedScopes.value.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('add').tr(),
|
||||
onTap: showAddScopeDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text('requirePkce'.tr()),
|
||||
value: requirePkce.value,
|
||||
onChanged: (value) =>
|
||||
requirePkce.value = value,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('allowOfflineAccess'.tr()),
|
||||
value: allowOfflineAccess.value,
|
||||
onChanged: (value) =>
|
||||
allowOfflineAccess.value = value,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, bottom: 24),
|
||||
isExpanded: oauthEnabled.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||
),
|
||||
body: bodyContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
88
lib/developers/developers/edit_app.g.dart
Normal file
88
lib/developers/developers/edit_app.g.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'edit_app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(customApp)
|
||||
final customAppProvider = CustomAppFamily._();
|
||||
|
||||
final class CustomAppProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CustomApp?>,
|
||||
CustomApp?,
|
||||
FutureOr<CustomApp?>
|
||||
>
|
||||
with $FutureModifier<CustomApp?>, $FutureProvider<CustomApp?> {
|
||||
CustomAppProvider._({
|
||||
required CustomAppFamily super.from,
|
||||
required (String, String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'customAppProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customAppHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'customAppProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<CustomApp?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<CustomApp?> create(Ref ref) {
|
||||
final argument = this.argument as (String, String, String);
|
||||
return customApp(ref, argument.$1, argument.$2, argument.$3);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CustomAppProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$customAppHash() => r'8e1b38f3dc9b04fad362ee1141fcbfc53f008c09';
|
||||
|
||||
final class CustomAppFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<CustomApp?>,
|
||||
(String, String, String)
|
||||
> {
|
||||
CustomAppFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'customAppProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
CustomAppProvider call(String publisherName, String projectId, String id) =>
|
||||
CustomAppProvider._(argument: (publisherName, projectId, id), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'customAppProvider';
|
||||
}
|
||||
465
lib/developers/developers/edit_bot.dart
Normal file
465
lib/developers/developers/edit_bot.dart
Normal file
@@ -0,0 +1,465 @@
|
||||
import 'package:croppy/croppy.dart' hide cropImage;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/core/services/image.dart';
|
||||
import 'package:island/developers/developers_models/bot.dart';
|
||||
import 'package:island/drive/drive_models/file.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/drive/drive_service.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'edit_bot.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<Bot?> bot(
|
||||
Ref ref,
|
||||
String publisherName,
|
||||
String projectId,
|
||||
String id,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
|
||||
);
|
||||
return Bot.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class EditBotScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final String? id;
|
||||
final bool isModal;
|
||||
const EditBotScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.id,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isNew = id == null;
|
||||
final botData = isNew
|
||||
? null
|
||||
: ref.watch(botProvider(publisherName, projectId, id!));
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final submitting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final nickController = useTextEditingController();
|
||||
final slugController = useTextEditingController();
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
|
||||
final firstNameController = useTextEditingController();
|
||||
final middleNameController = useTextEditingController();
|
||||
final lastNameController = useTextEditingController();
|
||||
final genderController = useTextEditingController();
|
||||
final pronounsController = useTextEditingController();
|
||||
final locationController = useTextEditingController();
|
||||
final timeZoneController = useTextEditingController();
|
||||
final bioController = useTextEditingController();
|
||||
final birthday = useState<DateTime?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (botData?.value != null) {
|
||||
nameController.text = botData!.value!.account.name;
|
||||
nickController.text = botData.value!.account.nick;
|
||||
slugController.text = botData.value!.slug;
|
||||
picture.value = botData.value!.account.profile.picture;
|
||||
background.value = botData.value!.account.profile.background;
|
||||
|
||||
// Populate from botData.value.account.profile
|
||||
firstNameController.text = botData.value!.account.profile.firstName;
|
||||
middleNameController.text = botData.value!.account.profile.middleName;
|
||||
lastNameController.text = botData.value!.account.profile.lastName;
|
||||
genderController.text = botData.value!.account.profile.gender;
|
||||
pronounsController.text = botData.value!.account.profile.pronouns;
|
||||
locationController.text = botData.value!.account.profile.location;
|
||||
timeZoneController.text = botData.value!.account.profile.timeZone;
|
||||
bioController.text = botData.value!.account.profile.bio;
|
||||
birthday.value = botData.value!.account.profile.birthday?.toLocal();
|
||||
}
|
||||
return null;
|
||||
}, [botData]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile = await FileUploader.createCloudFile(
|
||||
ref: ref,
|
||||
fileData: UniversalFile(data: result, type: UniversalFileType.image),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void performAction() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final data = {
|
||||
'name': nameController.text,
|
||||
'nick': nickController.text,
|
||||
'slug': slugController.text,
|
||||
'picture_id': picture.value?.id,
|
||||
'background_id': background.value?.id,
|
||||
'first_name': firstNameController.text,
|
||||
'middle_name': middleNameController.text,
|
||||
'last_name': lastNameController.text,
|
||||
'gender': genderController.text,
|
||||
'pronouns': pronounsController.text,
|
||||
'location': locationController.text,
|
||||
'time_zone': timeZoneController.text,
|
||||
'bio': bioController.text,
|
||||
'birthday': birthday.value?.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
if (isNew) {
|
||||
await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots',
|
||||
data: data,
|
||||
);
|
||||
} else {
|
||||
await client.patch(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
|
||||
final bodyContent = botData == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: botData?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: botData!.error,
|
||||
onRetry: () =>
|
||||
ref.invalidate(botProvider(publisherName, projectId, id!)),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
file: picture.value,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nickController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'nickname'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: bioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'bio'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'firstName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: middleNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'middleName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'lastName'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: genderController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gender'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: pronounsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'pronouns'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: locationController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'location'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: timeZoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'timeZone'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: birthday.value ?? DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (date != null) {
|
||||
birthday.value = date;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'birthday'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
birthday.value != null
|
||||
? DateFormat.yMMMd().format(birthday.value!)
|
||||
: 'Select a date'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges').tr(),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (isModal) {
|
||||
return bodyContent;
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||
body: bodyContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/developers/developers/edit_bot.g.dart
Normal file
79
lib/developers/developers/edit_bot.g.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'edit_bot.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(bot)
|
||||
final botProvider = BotFamily._();
|
||||
|
||||
final class BotProvider
|
||||
extends $FunctionalProvider<AsyncValue<Bot?>, Bot?, FutureOr<Bot?>>
|
||||
with $FutureModifier<Bot?>, $FutureProvider<Bot?> {
|
||||
BotProvider._({
|
||||
required BotFamily super.from,
|
||||
required (String, String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'botProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$botHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'botProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<Bot?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<Bot?> create(Ref ref) {
|
||||
final argument = this.argument as (String, String, String);
|
||||
return bot(ref, argument.$1, argument.$2, argument.$3);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BotProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$botHash() => r'7bec47bb2a4061a5babc6d6d19c3d4c320c91188';
|
||||
|
||||
final class BotFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<Bot?>, (String, String, String)> {
|
||||
BotFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'botProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
BotProvider call(String publisherName, String projectId, String id) =>
|
||||
BotProvider._(argument: (publisherName, projectId, id), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'botProvider';
|
||||
}
|
||||
129
lib/developers/developers/edit_project.dart
Normal file
129
lib/developers/developers/edit_project.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/developers/developers/hub.dart';
|
||||
import 'package:island/developers/developers_models/dev_project.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'edit_project.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<DevProject?> devProject(Ref ref, String pubName, String id) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/develop/developers/$pubName/projects/$id');
|
||||
return DevProject.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class EditProjectScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String? id;
|
||||
const EditProjectScreen({super.key, required this.publisherName, this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isNew = id == null;
|
||||
final projectData = isNew
|
||||
? null
|
||||
: ref.watch(devProjectProvider(publisherName, id!));
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final submitting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final slugController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
|
||||
useEffect(() {
|
||||
if (projectData?.value != null) {
|
||||
nameController.text = projectData!.value!.name;
|
||||
slugController.text = projectData.value!.slug;
|
||||
descriptionController.text = projectData.value!.description ?? '';
|
||||
}
|
||||
return null;
|
||||
}, [projectData]);
|
||||
|
||||
void performAction() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final data = {
|
||||
'name': nameController.text,
|
||||
'slug': slugController.text,
|
||||
'description': descriptionController.text,
|
||||
};
|
||||
if (isNew) {
|
||||
await client.post(
|
||||
'/develop/developers/$publisherName/projects',
|
||||
data: data,
|
||||
);
|
||||
} else {
|
||||
await client.put(
|
||||
'/develop/developers/$publisherName/projects/$id',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
ref.invalidate(devProjectsProvider(publisherName));
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'createProject'.tr() : 'editProject'.tr()),
|
||||
),
|
||||
body: projectData == null && !isNew
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: projectData?.hasError == true && !isNew
|
||||
? ResponseErrorWidget(
|
||||
error: projectData!.error,
|
||||
onRetry: () =>
|
||||
ref.invalidate(devProjectProvider(publisherName, id!)),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/developers/developers/edit_project.g.dart
Normal file
85
lib/developers/developers/edit_project.g.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'edit_project.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(devProject)
|
||||
final devProjectProvider = DevProjectFamily._();
|
||||
|
||||
final class DevProjectProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<DevProject?>,
|
||||
DevProject?,
|
||||
FutureOr<DevProject?>
|
||||
>
|
||||
with $FutureModifier<DevProject?>, $FutureProvider<DevProject?> {
|
||||
DevProjectProvider._({
|
||||
required DevProjectFamily super.from,
|
||||
required (String, String) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'devProjectProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$devProjectHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'devProjectProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<DevProject?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<DevProject?> create(Ref ref) {
|
||||
final argument = this.argument as (String, String);
|
||||
return devProject(ref, argument.$1, argument.$2);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DevProjectProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$devProjectHash() => r'd92be3f5cdc510c2a377615ed5c70622a6842bf2';
|
||||
|
||||
final class DevProjectFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<DevProject?>, (String, String)> {
|
||||
DevProjectFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'devProjectProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
DevProjectProvider call(String pubName, String id) =>
|
||||
DevProjectProvider._(argument: (pubName, id), from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'devProjectProvider';
|
||||
}
|
||||
948
lib/developers/developers/hub.dart
Normal file
948
lib/developers/developers/hub.dart
Normal file
@@ -0,0 +1,948 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/creators/creators/publishers_form.dart';
|
||||
import 'package:island/developers/developers/project_detail_view.dart';
|
||||
import 'package:island/developers/developers_models/dev_project.dart';
|
||||
import 'package:island/developers/developers_models/developer.dart';
|
||||
import 'package:island/posts/posts_models/publisher.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/core/services/responsive.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/core/widgets/content/sheet.dart';
|
||||
import 'package:island/shared/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'hub.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
|
||||
if (uname == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/develop/developers/$uname/stats');
|
||||
return DeveloperStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnDeveloper>> developers(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/develop/developers');
|
||||
return resp.data
|
||||
.map((e) => SnDeveloper.fromJson(e))
|
||||
.cast<SnDeveloper>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<DevProject>> devProjects(Ref ref, String pubName) async {
|
||||
if (pubName.isEmpty) return [];
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/develop/developers/$pubName/projects');
|
||||
return (resp.data as List)
|
||||
.map((e) => DevProject.fromJson(e))
|
||||
.cast<DevProject>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class DeveloperHubScreen extends HookConsumerWidget {
|
||||
final String? initialPublisherName;
|
||||
final String? initialProjectId;
|
||||
|
||||
const DeveloperHubScreen({
|
||||
super.key,
|
||||
this.initialPublisherName,
|
||||
this.initialProjectId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final developers = ref.watch(developersProvider);
|
||||
final currentDeveloper = useState<SnDeveloper?>(
|
||||
developers.value?.firstOrNull,
|
||||
);
|
||||
|
||||
final projects = currentDeveloper.value?.publisher?.name != null
|
||||
? ref.watch(
|
||||
devProjectsProvider(currentDeveloper.value!.publisher!.name),
|
||||
)
|
||||
: const AsyncValue<List<DevProject>>.data([]);
|
||||
|
||||
final currentProject = useState<DevProject?>(
|
||||
projects.value?.where((p) => p.id == initialProjectId).firstOrNull,
|
||||
);
|
||||
|
||||
final developerStats = ref.watch(
|
||||
developerStatsProvider(currentDeveloper.value?.publisher?.name),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: _ConsoleAppBar(
|
||||
currentDeveloper: currentDeveloper.value,
|
||||
currentProject: currentProject.value,
|
||||
onProjectChanged: (value) {
|
||||
currentProject.value = value;
|
||||
},
|
||||
onDeveloperChanged: (value) {
|
||||
currentDeveloper.value = value;
|
||||
},
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Main Content
|
||||
if (currentProject.value != null)
|
||||
Expanded(
|
||||
child: ProjectDetailView(
|
||||
publisherName: currentDeveloper.value!.publisher!.name,
|
||||
project: currentProject.value!,
|
||||
onBackToHub: () {
|
||||
currentProject.value = null;
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: _MainContentSection(
|
||||
currentDeveloper: currentDeveloper.value,
|
||||
projects: projects,
|
||||
developerStats: developerStats,
|
||||
onProjectSelected: (project) {
|
||||
currentProject.value = project;
|
||||
},
|
||||
onDeveloperSelected: (developer) {
|
||||
currentDeveloper.value = developer;
|
||||
},
|
||||
onCreateProject: () {
|
||||
if (currentDeveloper.value != null) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName:
|
||||
currentDeveloper.value!.publisher!.name,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
devProjectsProvider(
|
||||
currentDeveloper.value!.publisher!.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsoleAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final SnDeveloper? currentDeveloper;
|
||||
final DevProject? currentProject;
|
||||
final ValueChanged<DevProject?> onProjectChanged;
|
||||
final ValueChanged<SnDeveloper?> onDeveloperChanged;
|
||||
|
||||
const _ConsoleAppBar({
|
||||
required this.currentDeveloper,
|
||||
required this.currentProject,
|
||||
required this.onProjectChanged,
|
||||
required this.onDeveloperChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(56);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
leading: const PageBackButton(backTo: '/account'),
|
||||
title: Text('developerHub').tr(),
|
||||
actions: [
|
||||
if (currentProject != null)
|
||||
ProjectSelector(
|
||||
currentDeveloper: currentDeveloper,
|
||||
currentProject: currentProject,
|
||||
onProjectChanged: onProjectChanged,
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Main Content Section
|
||||
class _MainContentSection extends HookConsumerWidget {
|
||||
final SnDeveloper? currentDeveloper;
|
||||
final AsyncValue<List<DevProject>> projects;
|
||||
final AsyncValue<DeveloperStats?> developerStats;
|
||||
final ValueChanged<DevProject> onProjectSelected;
|
||||
final ValueChanged<SnDeveloper> onDeveloperSelected;
|
||||
final VoidCallback onCreateProject;
|
||||
|
||||
const _MainContentSection({
|
||||
required this.currentDeveloper,
|
||||
required this.projects,
|
||||
required this.developerStats,
|
||||
required this.onProjectSelected,
|
||||
required this.onDeveloperSelected,
|
||||
required this.onCreateProject,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: developerStats.when(
|
||||
data: (stats) => currentDeveloper == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _DeveloperUnselectedWidget(
|
||||
onDeveloperSelected: onDeveloperSelected,
|
||||
),
|
||||
).center()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Developer Stats
|
||||
if (stats != null) ...[
|
||||
Text(
|
||||
'Overview',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
_DeveloperStatsWidget(stats: stats),
|
||||
const Gap(24),
|
||||
],
|
||||
|
||||
// Projects Section
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Projects',
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onCreateProject,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: const Text('Create Project'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A73E8),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
// Projects List
|
||||
projects.value?.isNotEmpty ?? false
|
||||
? Column(
|
||||
children: projects.value!
|
||||
.map(
|
||||
(project) => _ProjectListTile(
|
||||
project: project,
|
||||
publisherName:
|
||||
currentDeveloper!.publisher!.name,
|
||||
onProjectSelected: onProjectSelected,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(48),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'No projects available',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () {
|
||||
ref.invalidate(
|
||||
developerStatsProvider(currentDeveloper?.publisher?.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeveloperSelector extends HookConsumerWidget {
|
||||
final bool isReadOnly;
|
||||
final SnDeveloper? currentDeveloper;
|
||||
final ValueChanged<SnDeveloper?> onDeveloperChanged;
|
||||
|
||||
const DeveloperSelector({
|
||||
super.key,
|
||||
required this.isReadOnly,
|
||||
required this.currentDeveloper,
|
||||
required this.onDeveloperChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final developers = ref.watch(developersProvider);
|
||||
|
||||
final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
|
||||
data: (data) => data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnDeveloper>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
file: item.publisher?.picture,
|
||||
),
|
||||
title: Text(item.publisher!.nick),
|
||||
subtitle: Text('@${item.publisher!.name}'),
|
||||
trailing: currentDeveloper?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
loading: () => [],
|
||||
error: (_, _) => [],
|
||||
);
|
||||
|
||||
if (isReadOnly || currentDeveloper == null) {
|
||||
return ProfilePictureWidget(
|
||||
radius: 16,
|
||||
file: currentDeveloper?.publisher?.picture,
|
||||
).center().padding(right: 8);
|
||||
}
|
||||
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnDeveloper>(
|
||||
alignment: Alignment.centerRight,
|
||||
value: currentDeveloper,
|
||||
hint: CircleAvatar(
|
||||
radius: 16,
|
||||
child: Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withOpacity(0.9),
|
||||
fill: 1,
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
items: [...developersMenu],
|
||||
onChanged: onDeveloperChanged,
|
||||
selectedItemBuilder: (context) {
|
||||
return [
|
||||
...developersMenu.map(
|
||||
(e) => ProfilePictureWidget(
|
||||
radius: 16,
|
||||
file: e.value?.publisher?.picture,
|
||||
).center().padding(right: 8),
|
||||
),
|
||||
];
|
||||
},
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 64,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectSelector extends HookConsumerWidget {
|
||||
final SnDeveloper? currentDeveloper;
|
||||
final DevProject? currentProject;
|
||||
final ValueChanged<DevProject?> onProjectChanged;
|
||||
|
||||
const ProjectSelector({
|
||||
super.key,
|
||||
required this.currentDeveloper,
|
||||
required this.currentProject,
|
||||
required this.onProjectChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (currentDeveloper == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final projects = ref.watch(
|
||||
devProjectsProvider(currentDeveloper!.publisher!.name),
|
||||
);
|
||||
|
||||
if (projects.value == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<DropdownMenuItem<DevProject>> projectsMenu = projects.value!
|
||||
.map(
|
||||
(item) => DropdownMenuItem<DevProject>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
item.name.isNotEmpty ? item.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(item.name),
|
||||
subtitle: Text(
|
||||
item.description ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: currentProject?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<DevProject>(
|
||||
value: currentProject,
|
||||
hint: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
'?',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
items: projectsMenu,
|
||||
onChanged: onProjectChanged,
|
||||
selectedItemBuilder: (context) {
|
||||
final isWider = isWiderScreen(context);
|
||||
return projectsMenu
|
||||
.map(
|
||||
(e) => isWider
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
e.value?.name ?? '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(right: 8)
|
||||
: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 64,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProjectListTile extends HookConsumerWidget {
|
||||
final DevProject project;
|
||||
final String publisherName;
|
||||
final ValueChanged<DevProject>? onProjectSelected;
|
||||
|
||||
const _ProjectListTile({
|
||||
required this.project,
|
||||
required this.publisherName,
|
||||
this.onProjectSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: const Icon(Symbols.folder_managed),
|
||||
title: Text(project.name),
|
||||
subtitle: Text(project.description ?? ''),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 17),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text('delete', style: const TextStyle(color: Colors.red)).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName: publisherName,
|
||||
project: project,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(devProjectsProvider(publisherName));
|
||||
}
|
||||
});
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteProjectHint'.tr(),
|
||||
'deleteProject'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/${project.id}',
|
||||
);
|
||||
ref.invalidate(devProjectsProvider(publisherName));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
onProjectSelected?.call(project);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeveloperStatsWidget extends StatelessWidget {
|
||||
final DeveloperStats stats;
|
||||
const _DeveloperStatsWidget({required this.stats});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.totalCustomApps.toString(),
|
||||
'totalCustomApps',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCard(
|
||||
BuildContext context,
|
||||
String statValue,
|
||||
String statLabel,
|
||||
) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
statValue,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
statLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeveloperUnselectedWidget extends HookConsumerWidget {
|
||||
final ValueChanged<SnDeveloper> onDeveloperSelected;
|
||||
|
||||
const _DeveloperUnselectedWidget({required this.onDeveloperSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final developers = ref.watch(developersProvider);
|
||||
|
||||
final hasDevelopers = developers.value?.isNotEmpty ?? false;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!hasDevelopers) ...[
|
||||
if (developers.isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: const CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
...([
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'developerHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
]),
|
||||
const Gap(24),
|
||||
],
|
||||
if (hasDevelopers)
|
||||
...(developers.value?.map(
|
||||
(developer) => ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: ProfilePictureWidget(
|
||||
file: developer.publisher?.picture,
|
||||
),
|
||||
title: Text(developer.publisher!.nick),
|
||||
subtitle: Text('@${developer.publisher!.name}'),
|
||||
onTap: () => onDeveloperSelected(developer),
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: const CircleAvatar(child: Icon(Symbols.add)),
|
||||
title: Text('enrollDeveloper').tr(),
|
||||
subtitle: Text('enrollDeveloperHint').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => const _DeveloperEnrollmentSheet(),
|
||||
).then((value) {
|
||||
if (value == true) {
|
||||
ref.invalidate(developersProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectForm extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final DevProject? project;
|
||||
|
||||
const ProjectForm({super.key, required this.publisherName, this.project});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isEditing = project != null;
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final nameController = useTextEditingController(text: project?.name ?? '');
|
||||
final slugController = useTextEditingController(text: project?.slug ?? '');
|
||||
final descriptionController = useTextEditingController(
|
||||
text: project?.description ?? '',
|
||||
);
|
||||
final submitting = useState(false);
|
||||
|
||||
Future<void> submit() async {
|
||||
if (!(formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
try {
|
||||
submitting.value = true;
|
||||
final client = ref.read(apiClientProvider);
|
||||
final data = {
|
||||
'name': nameController.text,
|
||||
'slug': slugController.text,
|
||||
'description': descriptionController.text,
|
||||
};
|
||||
|
||||
final resp = isEditing
|
||||
? await client.put(
|
||||
'/develop/developers/$publisherName/projects/${project!.id}',
|
||||
data: data,
|
||||
)
|
||||
: await client.post(
|
||||
'/develop/developers/$publisherName/projects',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(DevProject.fromJson(resp.data));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
helperText: 'slugHint'.tr(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(isEditing ? 'saveChanges'.tr() : 'create'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeveloperEnrollmentSheet extends HookConsumerWidget {
|
||||
const _DeveloperEnrollmentSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
|
||||
Future<void> enroll(SnPublisher publisher) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/develop/developers/${publisher.name}/enroll');
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'enrollDeveloper'.tr(),
|
||||
child: publishers.when(
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'noDevelopersToEnroll',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final publisher = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
file: publisher.picture,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () => enroll(publisher),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(publishersManagedProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
201
lib/developers/developers/hub.g.dart
Normal file
201
lib/developers/developers/hub.g.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hub.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(developerStats)
|
||||
final developerStatsProvider = DeveloperStatsFamily._();
|
||||
|
||||
final class DeveloperStatsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<DeveloperStats?>,
|
||||
DeveloperStats?,
|
||||
FutureOr<DeveloperStats?>
|
||||
>
|
||||
with $FutureModifier<DeveloperStats?>, $FutureProvider<DeveloperStats?> {
|
||||
DeveloperStatsProvider._({
|
||||
required DeveloperStatsFamily super.from,
|
||||
required String? super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'developerStatsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$developerStatsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'developerStatsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<DeveloperStats?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<DeveloperStats?> create(Ref ref) {
|
||||
final argument = this.argument as String?;
|
||||
return developerStats(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DeveloperStatsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$developerStatsHash() => r'45546f29ec7cd1a9c3a4e0f4e39275e78bf34755';
|
||||
|
||||
final class DeveloperStatsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<DeveloperStats?>, String?> {
|
||||
DeveloperStatsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'developerStatsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
DeveloperStatsProvider call(String? uname) =>
|
||||
DeveloperStatsProvider._(argument: uname, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'developerStatsProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(developers)
|
||||
final developersProvider = DevelopersProvider._();
|
||||
|
||||
final class DevelopersProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnDeveloper>>,
|
||||
List<SnDeveloper>,
|
||||
FutureOr<List<SnDeveloper>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SnDeveloper>>,
|
||||
$FutureProvider<List<SnDeveloper>> {
|
||||
DevelopersProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'developersProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$developersHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnDeveloper>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnDeveloper>> create(Ref ref) {
|
||||
return developers(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2';
|
||||
|
||||
@ProviderFor(devProjects)
|
||||
final devProjectsProvider = DevProjectsFamily._();
|
||||
|
||||
final class DevProjectsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<DevProject>>,
|
||||
List<DevProject>,
|
||||
FutureOr<List<DevProject>>
|
||||
>
|
||||
with $FutureModifier<List<DevProject>>, $FutureProvider<List<DevProject>> {
|
||||
DevProjectsProvider._({
|
||||
required DevProjectsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'devProjectsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$devProjectsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'devProjectsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<DevProject>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<DevProject>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return devProjects(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DevProjectsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$devProjectsHash() => r'715b395bebda785d38691ffee3b88e50b498c91a';
|
||||
|
||||
final class DevProjectsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<DevProject>>, String> {
|
||||
DevProjectsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'devProjectsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
DevProjectsProvider call(String pubName) =>
|
||||
DevProjectsProvider._(argument: pubName, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'devProjectsProvider';
|
||||
}
|
||||
23
lib/developers/developers/new_app.dart
Normal file
23
lib/developers/developers/new_app.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/developers/developers/edit_app.dart';
|
||||
|
||||
class NewCustomAppScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final bool isModal;
|
||||
const NewCustomAppScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: isModal,
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/developers/developers/new_bot.dart
Normal file
23
lib/developers/developers/new_bot.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/developers/developers/edit_bot.dart';
|
||||
|
||||
class NewBotScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
final bool isModal;
|
||||
const NewBotScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
this.isModal = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: isModal,
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/developers/developers/new_project.dart
Normal file
12
lib/developers/developers/new_project.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/developers/developers/edit_project.dart';
|
||||
|
||||
class NewProjectScreen extends StatelessWidget {
|
||||
final String publisherName;
|
||||
const NewProjectScreen({super.key, required this.publisherName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditProjectScreen(publisherName: publisherName);
|
||||
}
|
||||
}
|
||||
149
lib/developers/developers/project_detail_view.dart
Normal file
149
lib/developers/developers/project_detail_view.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/developers/developers/apps.dart';
|
||||
import 'package:island/developers/developers/bots.dart';
|
||||
import 'package:island/developers/developers_models/dev_project.dart';
|
||||
import 'package:island/core/services/responsive.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class ProjectDetailView extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final DevProject project;
|
||||
final VoidCallback onBackToHub;
|
||||
|
||||
const ProjectDetailView({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.project,
|
||||
required this.onBackToHub,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final currentDest = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
tabController.addListener(() {
|
||||
if (tabController.indexIsChanging) {
|
||||
currentDest.value = tabController.index;
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
if (isWide) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Card(
|
||||
margin: const EdgeInsets.only(left: 16, bottom: 16, top: 12),
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -56),
|
||||
child: NavigationRail(
|
||||
extended: isWiderScreen(context),
|
||||
scrollable: true,
|
||||
labelType: isWiderScreen(context)
|
||||
? null
|
||||
: NavigationRailLabelType.selected,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedIndex: currentDest.value,
|
||||
onDestinationSelected: (index) =>
|
||||
tabController.animateTo(index),
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.apps),
|
||||
label: Text('customApps'.tr()),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.smart_toy),
|
||||
label: Text('bots'.tr()),
|
||||
),
|
||||
],
|
||||
leading: Container(
|
||||
width: isWiderScreen(context) ? 256 : 80,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 8,
|
||||
top: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: onBackToHub,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
iconSize: 16,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
if (isWiderScreen(context))
|
||||
Expanded(child: Text("backToHub").tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
CustomAppsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: project.id,
|
||||
),
|
||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text('customApps'.tr(), textAlign: TextAlign.center),
|
||||
),
|
||||
Tab(child: Text('bots'.tr(), textAlign: TextAlign.center)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
CustomAppsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: project.id,
|
||||
),
|
||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
lib/developers/developers_models/bot.dart
Normal file
63
lib/developers/developers_models/bot.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/accounts/accounts_models/account.dart';
|
||||
import 'package:island/developers/developers_models/developer.dart';
|
||||
|
||||
part 'bot.freezed.dart';
|
||||
part 'bot.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class Bot with _$Bot {
|
||||
const factory Bot({
|
||||
required String id,
|
||||
required String slug,
|
||||
required bool isActive,
|
||||
required String projectId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required SnAccount account,
|
||||
SnDeveloper? developer,
|
||||
}) = _Bot;
|
||||
|
||||
factory Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BotConfig with _$BotConfig {
|
||||
const factory BotConfig({
|
||||
@Default(false) bool isPublic,
|
||||
@Default(false) bool isInteractive,
|
||||
@Default([]) List<String> allowedRealms,
|
||||
@Default([]) List<String> allowedChatTypes,
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _BotConfig;
|
||||
|
||||
factory BotConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$BotConfigFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BotLinks with _$BotLinks {
|
||||
const factory BotLinks({
|
||||
String? website,
|
||||
String? documentation,
|
||||
String? privacyPolicy,
|
||||
String? termsOfService,
|
||||
}) = _BotLinks;
|
||||
|
||||
factory BotLinks.fromJson(Map<String, dynamic> json) =>
|
||||
_$BotLinksFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BotSecret with _$BotSecret {
|
||||
const factory BotSecret({
|
||||
@Default('') String id,
|
||||
@Default('') String secret,
|
||||
String? description,
|
||||
DateTime? expiredAt,
|
||||
@Default('') String botId,
|
||||
}) = _BotSecret;
|
||||
|
||||
factory BotSecret.fromJson(Map<String, dynamic> json) =>
|
||||
_$BotSecretFromJson(json);
|
||||
}
|
||||
1156
lib/developers/developers_models/bot.freezed.dart
Normal file
1156
lib/developers/developers_models/bot.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
89
lib/developers/developers_models/bot.g.dart
Normal file
89
lib/developers/developers_models/bot.g.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bot.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_Bot _$BotFromJson(Map<String, dynamic> json) => _Bot(
|
||||
id: json['id'] as String,
|
||||
slug: json['slug'] as String,
|
||||
isActive: json['is_active'] as bool,
|
||||
projectId: json['project_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
developer: json['developer'] == null
|
||||
? null
|
||||
: SnDeveloper.fromJson(json['developer'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
'is_active': instance.isActive,
|
||||
'project_id': instance.projectId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'account': instance.account.toJson(),
|
||||
'developer': instance.developer?.toJson(),
|
||||
};
|
||||
|
||||
_BotConfig _$BotConfigFromJson(Map<String, dynamic> json) => _BotConfig(
|
||||
isPublic: json['is_public'] as bool? ?? false,
|
||||
isInteractive: json['is_interactive'] as bool? ?? false,
|
||||
allowedRealms:
|
||||
(json['allowed_realms'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
allowedChatTypes:
|
||||
(json['allowed_chat_types'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BotConfigToJson(_BotConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'is_public': instance.isPublic,
|
||||
'is_interactive': instance.isInteractive,
|
||||
'allowed_realms': instance.allowedRealms,
|
||||
'allowed_chat_types': instance.allowedChatTypes,
|
||||
'metadata': instance.metadata,
|
||||
};
|
||||
|
||||
_BotLinks _$BotLinksFromJson(Map<String, dynamic> json) => _BotLinks(
|
||||
website: json['website'] as String?,
|
||||
documentation: json['documentation'] as String?,
|
||||
privacyPolicy: json['privacy_policy'] as String?,
|
||||
termsOfService: json['terms_of_service'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BotLinksToJson(_BotLinks instance) => <String, dynamic>{
|
||||
'website': instance.website,
|
||||
'documentation': instance.documentation,
|
||||
'privacy_policy': instance.privacyPolicy,
|
||||
'terms_of_service': instance.termsOfService,
|
||||
};
|
||||
|
||||
_BotSecret _$BotSecretFromJson(Map<String, dynamic> json) => _BotSecret(
|
||||
id: json['id'] as String? ?? '',
|
||||
secret: json['secret'] as String? ?? '',
|
||||
description: json['description'] as String?,
|
||||
expiredAt: json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
botId: json['bot_id'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BotSecretToJson(_BotSecret instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'secret': instance.secret,
|
||||
'description': instance.description,
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'bot_id': instance.botId,
|
||||
};
|
||||
20
lib/developers/developers_models/bot_key.dart
Normal file
20
lib/developers/developers_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/developers/developers_models/bot_key.freezed.dart
Normal file
289
lib/developers/developers_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/developers/developers_models/bot_key.g.dart
Normal file
29
lib/developers/developers_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,
|
||||
};
|
||||
71
lib/developers/developers_models/custom_app.dart
Normal file
71
lib/developers/developers_models/custom_app.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/drive/drive_models/file.dart';
|
||||
import 'package:island/accounts/accounts_models/account.dart';
|
||||
|
||||
part 'custom_app.freezed.dart';
|
||||
part 'custom_app.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class CustomApp with _$CustomApp {
|
||||
const factory CustomApp({
|
||||
@Default('') String id,
|
||||
@Default('') String slug,
|
||||
@Default('') String name,
|
||||
String? description,
|
||||
@Default(0) int status,
|
||||
SnCloudFile? picture,
|
||||
SnCloudFile? background,
|
||||
SnVerificationMark? verification,
|
||||
CustomAppOauthConfig? oauthConfig,
|
||||
CustomAppLinks? links,
|
||||
@Default([]) List<CustomAppSecret> secrets,
|
||||
@Default('') String publisherId,
|
||||
}) = _CustomApp;
|
||||
|
||||
factory CustomApp.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomAppFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class CustomAppLinks with _$CustomAppLinks {
|
||||
const factory CustomAppLinks({
|
||||
String? homePage,
|
||||
String? privacyPolicy,
|
||||
String? termsOfService,
|
||||
}) = _CustomAppLinks;
|
||||
|
||||
factory CustomAppLinks.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomAppLinksFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class CustomAppOauthConfig with _$CustomAppOauthConfig {
|
||||
const factory CustomAppOauthConfig({
|
||||
String? clientUri,
|
||||
@Default([]) List<String> redirectUris,
|
||||
List<String>? postLogoutRedirectUris,
|
||||
@Default(['openid', 'profile', 'email']) List<String> allowedScopes,
|
||||
@Default(['authorization_code', 'refresh_token'])
|
||||
List<String> allowedGrantTypes,
|
||||
@Default(true) bool requirePkce,
|
||||
@Default(false) bool allowOfflineAccess,
|
||||
}) = _CustomAppOauthConfig;
|
||||
|
||||
factory CustomAppOauthConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomAppOauthConfigFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class CustomAppSecret with _$CustomAppSecret {
|
||||
const factory CustomAppSecret({
|
||||
@Default('') String id,
|
||||
@Default('') String secret,
|
||||
String? description,
|
||||
DateTime? expiredAt,
|
||||
@Default(false) bool isOidc,
|
||||
@Default('') String appId,
|
||||
}) = _CustomAppSecret;
|
||||
|
||||
factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomAppSecretFromJson(json);
|
||||
}
|
||||
1266
lib/developers/developers_models/custom_app.freezed.dart
Normal file
1266
lib/developers/developers_models/custom_app.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
130
lib/developers/developers_models/custom_app.g.dart
Normal file
130
lib/developers/developers_models/custom_app.g.dart
Normal file
@@ -0,0 +1,130 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'custom_app.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_CustomApp _$CustomAppFromJson(Map<String, dynamic> json) => _CustomApp(
|
||||
id: json['id'] as String? ?? '',
|
||||
slug: json['slug'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
description: json['description'] as String?,
|
||||
status: (json['status'] as num?)?.toInt() ?? 0,
|
||||
picture: json['picture'] == null
|
||||
? null
|
||||
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
|
||||
background: json['background'] == null
|
||||
? null
|
||||
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||
verification: json['verification'] == null
|
||||
? null
|
||||
: SnVerificationMark.fromJson(
|
||||
json['verification'] as Map<String, dynamic>,
|
||||
),
|
||||
oauthConfig: json['oauth_config'] == null
|
||||
? null
|
||||
: CustomAppOauthConfig.fromJson(
|
||||
json['oauth_config'] as Map<String, dynamic>,
|
||||
),
|
||||
links: json['links'] == null
|
||||
? null
|
||||
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
|
||||
secrets:
|
||||
(json['secrets'] as List<dynamic>?)
|
||||
?.map((e) => CustomAppSecret.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
publisherId: json['publisher_id'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomAppToJson(_CustomApp instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'status': instance.status,
|
||||
'picture': instance.picture?.toJson(),
|
||||
'background': instance.background?.toJson(),
|
||||
'verification': instance.verification?.toJson(),
|
||||
'oauth_config': instance.oauthConfig?.toJson(),
|
||||
'links': instance.links?.toJson(),
|
||||
'secrets': instance.secrets.map((e) => e.toJson()).toList(),
|
||||
'publisher_id': instance.publisherId,
|
||||
};
|
||||
|
||||
_CustomAppLinks _$CustomAppLinksFromJson(Map<String, dynamic> json) =>
|
||||
_CustomAppLinks(
|
||||
homePage: json['home_page'] as String?,
|
||||
privacyPolicy: json['privacy_policy'] as String?,
|
||||
termsOfService: json['terms_of_service'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomAppLinksToJson(_CustomAppLinks instance) =>
|
||||
<String, dynamic>{
|
||||
'home_page': instance.homePage,
|
||||
'privacy_policy': instance.privacyPolicy,
|
||||
'terms_of_service': instance.termsOfService,
|
||||
};
|
||||
|
||||
_CustomAppOauthConfig _$CustomAppOauthConfigFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _CustomAppOauthConfig(
|
||||
clientUri: json['client_uri'] as String?,
|
||||
redirectUris:
|
||||
(json['redirect_uris'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
postLogoutRedirectUris: (json['post_logout_redirect_uris'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
allowedScopes:
|
||||
(json['allowed_scopes'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const ['openid', 'profile', 'email'],
|
||||
allowedGrantTypes:
|
||||
(json['allowed_grant_types'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const ['authorization_code', 'refresh_token'],
|
||||
requirePkce: json['require_pkce'] as bool? ?? true,
|
||||
allowOfflineAccess: json['allow_offline_access'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomAppOauthConfigToJson(
|
||||
_CustomAppOauthConfig instance,
|
||||
) => <String, dynamic>{
|
||||
'client_uri': instance.clientUri,
|
||||
'redirect_uris': instance.redirectUris,
|
||||
'post_logout_redirect_uris': instance.postLogoutRedirectUris,
|
||||
'allowed_scopes': instance.allowedScopes,
|
||||
'allowed_grant_types': instance.allowedGrantTypes,
|
||||
'require_pkce': instance.requirePkce,
|
||||
'allow_offline_access': instance.allowOfflineAccess,
|
||||
};
|
||||
|
||||
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
|
||||
_CustomAppSecret(
|
||||
id: json['id'] as String? ?? '',
|
||||
secret: json['secret'] as String? ?? '',
|
||||
description: json['description'] as String?,
|
||||
expiredAt: json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
isOidc: json['is_oidc'] as bool? ?? false,
|
||||
appId: json['app_id'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'secret': instance.secret,
|
||||
'description': instance.description,
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'is_oidc': instance.isOidc,
|
||||
'app_id': instance.appId,
|
||||
};
|
||||
19
lib/developers/developers_models/custom_app_secret.dart
Normal file
19
lib/developers/developers_models/custom_app_secret.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'custom_app_secret.freezed.dart';
|
||||
part 'custom_app_secret.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class CustomAppSecret with _$CustomAppSecret {
|
||||
const factory CustomAppSecret({
|
||||
required String id,
|
||||
required String? secret,
|
||||
required DateTime createdAt,
|
||||
String? description,
|
||||
int? expiresIn,
|
||||
bool? isOidc,
|
||||
}) = _CustomAppSecret;
|
||||
|
||||
factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>
|
||||
_$CustomAppSecretFromJson(json);
|
||||
}
|
||||
286
lib/developers/developers_models/custom_app_secret.freezed.dart
Normal file
286
lib/developers/developers_models/custom_app_secret.freezed.dart
Normal file
@@ -0,0 +1,286 @@
|
||||
// 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 'custom_app_secret.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$CustomAppSecret {
|
||||
|
||||
String get id; String? get secret; DateTime get createdAt; String? get description; int? get expiresIn; bool? get isOidc;
|
||||
/// Create a copy of CustomAppSecret
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$CustomAppSecretCopyWith<CustomAppSecret> get copyWith => _$CustomAppSecretCopyWithImpl<CustomAppSecret>(this as CustomAppSecret, _$identity);
|
||||
|
||||
/// Serializes this CustomAppSecret to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $CustomAppSecretCopyWith<$Res> {
|
||||
factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$CustomAppSecretCopyWithImpl<$Res>
|
||||
implements $CustomAppSecretCopyWith<$Res> {
|
||||
_$CustomAppSecretCopyWithImpl(this._self, this._then);
|
||||
|
||||
final CustomAppSecret _self;
|
||||
final $Res Function(CustomAppSecret) _then;
|
||||
|
||||
/// Create a copy of CustomAppSecret
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
|
||||
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [CustomAppSecret].
|
||||
extension CustomAppSecretPatterns on CustomAppSecret {
|
||||
/// 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( _CustomAppSecret value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret() 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( _CustomAppSecret value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret():
|
||||
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( _CustomAppSecret value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret() 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? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret() when $default != null:
|
||||
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);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? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret():
|
||||
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);}
|
||||
}
|
||||
/// 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? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CustomAppSecret() when $default != null:
|
||||
return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _CustomAppSecret implements CustomAppSecret {
|
||||
const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description, this.expiresIn, this.isOidc});
|
||||
factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String? secret;
|
||||
@override final DateTime createdAt;
|
||||
@override final String? description;
|
||||
@override final int? expiresIn;
|
||||
@override final bool? isOidc;
|
||||
|
||||
/// Create a copy of CustomAppSecret
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$CustomAppSecretCopyWith<_CustomAppSecret> get copyWith => __$CustomAppSecretCopyWithImpl<_CustomAppSecret>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$CustomAppSecretToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecretCopyWith<$Res> {
|
||||
factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$CustomAppSecretCopyWithImpl<$Res>
|
||||
implements _$CustomAppSecretCopyWith<$Res> {
|
||||
__$CustomAppSecretCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _CustomAppSecret _self;
|
||||
final $Res Function(_CustomAppSecret) _then;
|
||||
|
||||
/// Create a copy of CustomAppSecret
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
|
||||
return _then(_CustomAppSecret(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
|
||||
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
27
lib/developers/developers_models/custom_app_secret.g.dart
Normal file
27
lib/developers/developers_models/custom_app_secret.g.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'custom_app_secret.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
|
||||
_CustomAppSecret(
|
||||
id: json['id'] as String,
|
||||
secret: json['secret'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
description: json['description'] as String?,
|
||||
expiresIn: (json['expires_in'] as num?)?.toInt(),
|
||||
isOidc: json['is_oidc'] as bool?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'secret': instance.secret,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'description': instance.description,
|
||||
'expires_in': instance.expiresIn,
|
||||
'is_oidc': instance.isOidc,
|
||||
};
|
||||
23
lib/developers/developers_models/dev_project.dart
Normal file
23
lib/developers/developers_models/dev_project.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
class DevProject {
|
||||
final String id;
|
||||
final String slug;
|
||||
final String name;
|
||||
final String? description;
|
||||
|
||||
DevProject({
|
||||
required this.id,
|
||||
required this.slug,
|
||||
required this.name,
|
||||
this.description,
|
||||
});
|
||||
|
||||
factory DevProject.fromJson(Map<String, dynamic> json) {
|
||||
return DevProject(
|
||||
id: json['id'],
|
||||
slug: json['slug'],
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
);
|
||||
}
|
||||
}
|
||||
26
lib/developers/developers_models/developer.dart
Normal file
26
lib/developers/developers_models/developer.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/posts/posts_models/publisher.dart';
|
||||
|
||||
part 'developer.freezed.dart';
|
||||
part 'developer.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnDeveloper with _$SnDeveloper {
|
||||
const factory SnDeveloper({
|
||||
required String id,
|
||||
required String publisherId,
|
||||
SnPublisher? publisher,
|
||||
}) = _SnDeveloper;
|
||||
|
||||
factory SnDeveloper.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnDeveloperFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class DeveloperStats with _$DeveloperStats {
|
||||
const factory DeveloperStats({@Default(0) int totalCustomApps}) =
|
||||
_DeveloperStats;
|
||||
|
||||
factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$DeveloperStatsFromJson(json);
|
||||
}
|
||||
558
lib/developers/developers_models/developer.freezed.dart
Normal file
558
lib/developers/developers_models/developer.freezed.dart
Normal file
@@ -0,0 +1,558 @@
|
||||
// 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 'developer.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnDeveloper {
|
||||
|
||||
String get id; String get publisherId; SnPublisher? get publisher;
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnDeveloperCopyWith<SnDeveloper> get copyWith => _$SnDeveloperCopyWithImpl<SnDeveloper>(this as SnDeveloper, _$identity);
|
||||
|
||||
/// Serializes this SnDeveloper to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnDeveloperCopyWith<$Res> {
|
||||
factory $SnDeveloperCopyWith(SnDeveloper value, $Res Function(SnDeveloper) _then) = _$SnDeveloperCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String publisherId, SnPublisher? publisher
|
||||
});
|
||||
|
||||
|
||||
$SnPublisherCopyWith<$Res>? get publisher;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnDeveloperCopyWithImpl<$Res>
|
||||
implements $SnDeveloperCopyWith<$Res> {
|
||||
_$SnDeveloperCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnDeveloper _self;
|
||||
final $Res Function(SnDeveloper) _then;
|
||||
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
|
||||
as SnPublisher?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPublisherCopyWith<$Res>? get publisher {
|
||||
if (_self.publisher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
|
||||
return _then(_self.copyWith(publisher: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnDeveloper].
|
||||
extension SnDeveloperPatterns on SnDeveloper {
|
||||
/// 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( _SnDeveloper value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper() 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( _SnDeveloper value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper():
|
||||
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( _SnDeveloper value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper() 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 publisherId, SnPublisher? publisher)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper() when $default != null:
|
||||
return $default(_that.id,_that.publisherId,_that.publisher);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 publisherId, SnPublisher? publisher) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper():
|
||||
return $default(_that.id,_that.publisherId,_that.publisher);}
|
||||
}
|
||||
/// 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 publisherId, SnPublisher? publisher)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnDeveloper() when $default != null:
|
||||
return $default(_that.id,_that.publisherId,_that.publisher);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnDeveloper implements SnDeveloper {
|
||||
const _SnDeveloper({required this.id, required this.publisherId, this.publisher});
|
||||
factory _SnDeveloper.fromJson(Map<String, dynamic> json) => _$SnDeveloperFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String publisherId;
|
||||
@override final SnPublisher? publisher;
|
||||
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnDeveloperCopyWith<_SnDeveloper> get copyWith => __$SnDeveloperCopyWithImpl<_SnDeveloper>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnDeveloperToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,publisherId,publisher);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnDeveloperCopyWith<$Res> implements $SnDeveloperCopyWith<$Res> {
|
||||
factory _$SnDeveloperCopyWith(_SnDeveloper value, $Res Function(_SnDeveloper) _then) = __$SnDeveloperCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String publisherId, SnPublisher? publisher
|
||||
});
|
||||
|
||||
|
||||
@override $SnPublisherCopyWith<$Res>? get publisher;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnDeveloperCopyWithImpl<$Res>
|
||||
implements _$SnDeveloperCopyWith<$Res> {
|
||||
__$SnDeveloperCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnDeveloper _self;
|
||||
final $Res Function(_SnDeveloper) _then;
|
||||
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) {
|
||||
return _then(_SnDeveloper(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
|
||||
as SnPublisher?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnDeveloper
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPublisherCopyWith<$Res>? get publisher {
|
||||
if (_self.publisher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
|
||||
return _then(_self.copyWith(publisher: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$DeveloperStats {
|
||||
|
||||
int get totalCustomApps;
|
||||
/// Create a copy of DeveloperStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$DeveloperStatsCopyWith<DeveloperStats> get copyWith => _$DeveloperStatsCopyWithImpl<DeveloperStats>(this as DeveloperStats, _$identity);
|
||||
|
||||
/// Serializes this DeveloperStats to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,totalCustomApps);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $DeveloperStatsCopyWith<$Res> {
|
||||
factory $DeveloperStatsCopyWith(DeveloperStats value, $Res Function(DeveloperStats) _then) = _$DeveloperStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int totalCustomApps
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$DeveloperStatsCopyWithImpl<$Res>
|
||||
implements $DeveloperStatsCopyWith<$Res> {
|
||||
_$DeveloperStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final DeveloperStats _self;
|
||||
final $Res Function(DeveloperStats) _then;
|
||||
|
||||
/// Create a copy of DeveloperStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? totalCustomApps = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [DeveloperStats].
|
||||
extension DeveloperStatsPatterns on DeveloperStats {
|
||||
/// 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( _DeveloperStats value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats() 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( _DeveloperStats value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats():
|
||||
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( _DeveloperStats value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats() 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( int totalCustomApps)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats() when $default != null:
|
||||
return $default(_that.totalCustomApps);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( int totalCustomApps) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats():
|
||||
return $default(_that.totalCustomApps);}
|
||||
}
|
||||
/// 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( int totalCustomApps)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _DeveloperStats() when $default != null:
|
||||
return $default(_that.totalCustomApps);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _DeveloperStats implements DeveloperStats {
|
||||
const _DeveloperStats({this.totalCustomApps = 0});
|
||||
factory _DeveloperStats.fromJson(Map<String, dynamic> json) => _$DeveloperStatsFromJson(json);
|
||||
|
||||
@override@JsonKey() final int totalCustomApps;
|
||||
|
||||
/// Create a copy of DeveloperStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$DeveloperStatsCopyWith<_DeveloperStats> get copyWith => __$DeveloperStatsCopyWithImpl<_DeveloperStats>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$DeveloperStatsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,totalCustomApps);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$DeveloperStatsCopyWith<$Res> implements $DeveloperStatsCopyWith<$Res> {
|
||||
factory _$DeveloperStatsCopyWith(_DeveloperStats value, $Res Function(_DeveloperStats) _then) = __$DeveloperStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int totalCustomApps
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$DeveloperStatsCopyWithImpl<$Res>
|
||||
implements _$DeveloperStatsCopyWith<$Res> {
|
||||
__$DeveloperStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _DeveloperStats _self;
|
||||
final $Res Function(_DeveloperStats) _then;
|
||||
|
||||
/// Create a copy of DeveloperStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? totalCustomApps = null,}) {
|
||||
return _then(_DeveloperStats(
|
||||
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
30
lib/developers/developers_models/developer.g.dart
Normal file
30
lib/developers/developers_models/developer.g.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'developer.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper(
|
||||
id: json['id'] as String,
|
||||
publisherId: json['publisher_id'] as String,
|
||||
publisher: json['publisher'] == null
|
||||
? null
|
||||
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'publisher_id': instance.publisherId,
|
||||
'publisher': instance.publisher?.toJson(),
|
||||
};
|
||||
|
||||
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
|
||||
_DeveloperStats(
|
||||
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DeveloperStatsToJson(_DeveloperStats instance) =>
|
||||
<String, dynamic>{'total_custom_apps': instance.totalCustomApps};
|
||||
Reference in New Issue
Block a user