🎨 Use feature based folder structure

This commit is contained in:
2026-02-06 00:37:02 +08:00
parent 62a3ea26e3
commit 862e3b451b
539 changed files with 8406 additions and 5056 deletions

View 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),
);
}
}

View 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),
),
),
);
}
}

View 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';
}

View 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)),
),
);
}
}

View 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';
}

View 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),
);
}
}

View 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)),
),
);
}
}

View 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';
}

View 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)),
),
);
}
}

View 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';
}

View 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,
);
}
}

View 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';
}

View 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,
);
}
}

View 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';
}

View 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),
),
),
);
}
}

View 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';
}

View 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),
),
),
);
}
}

View 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';
}

View 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,
);
}
}

View 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,
);
}
}

View 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);
}
}

View 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),
),
],
);
}
}
}

View 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);
}

File diff suppressed because it is too large Load Diff

View 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,
};

View 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);
}

View 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

View 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,
};

View 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);
}

File diff suppressed because it is too large Load Diff

View 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,
};

View 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);
}

View 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

View 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,
};

View 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'],
);
}
}

View 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);
}

View 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

View 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};