diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 11a6662c..c164c5a3 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -643,6 +643,14 @@ "enrollDeveloperHint": "Enroll one of your publishers to become a developer.", "noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.", "totalCustomApps": "Total Custom Apps", + "projects": "Projects", + "noProjects": "No projects found.", + "deleteProject": "Delete Project", + "deleteProjectHint": "Are you sure you want to delete this project? This action cannot be undone.", + "createProject": "Create Project", + "editProject": "Edit Project", + "projectDetails": "Project Details", + "createBot": "Create Bot", "customApps": "Custom Apps", "noCustomApps": "No custom apps yet.", "createCustomApp": "Create Custom App", diff --git a/lib/models/dev_project.dart b/lib/models/dev_project.dart new file mode 100644 index 00000000..b066deb8 --- /dev/null +++ b/lib/models/dev_project.dart @@ -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 json) { + return DevProject( + id: json['id'], + slug: json['slug'], + name: json['name'], + description: json['description'], + ); + } +} diff --git a/lib/route.dart b/lib/route.dart index b60c4ba9..bb616140 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -7,12 +7,14 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/screens/about.dart'; import 'package:island/screens/account/credits.dart'; -import 'package:island/screens/developers/apps.dart'; -import 'package:island/screens/developers/bots.dart'; import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/edit_bot.dart'; import 'package:island/screens/developers/new_app.dart'; import 'package:island/screens/developers/hub.dart'; +import 'package:island/screens/developers/projects.dart'; +import 'package:island/screens/developers/edit_project.dart'; +import 'package:island/screens/developers/new_project.dart'; +import 'package:island/screens/developers/project_detail.dart'; import 'package:island/screens/discovery/articles.dart'; import 'package:island/screens/posts/post_categories_list.dart'; import 'package:island/screens/posts/post_category_detail.dart'; @@ -293,102 +295,89 @@ final routerProvider = Provider((ref) { builder: (context, state) => const DeveloperHubScreen(), ), GoRoute( - name: 'developerApps', - path: '/developers/:name/apps', + name: 'developerProjects', + path: '/developers/:name/projects', builder: - (context, state) => CustomAppsScreen( + (context, state) => DevProjectsScreen( publisherName: state.pathParameters['name']!, ), ), GoRoute( - name: 'developerAppNew', - path: '/developers/:name/apps/new', + name: 'developerProjectNew', + path: '/developers/:name/projects/new', builder: - (context, state) => NewCustomAppScreen( + (context, state) => NewProjectScreen( publisherName: state.pathParameters['name']!, ), ), GoRoute( - name: 'developerAppEdit', - path: '/developers/:name/apps/:id', + name: 'developerProjectEdit', + path: '/developers/:name/projects/:id/edit', builder: - (context, state) => EditAppScreen( - publisherName: state.pathParameters['name']!, - id: state.pathParameters['id']!, - ), - ), - // Bot routes - GoRoute( - name: 'developerBots', - path: '/developers/:name/bots', - builder: - (context, state) => BotsScreen( - publisherName: state.pathParameters['name']!, - ), - ), - GoRoute( - name: 'developerBotsApp', - path: '/developers/:name/apps/:appId/bots', - builder: - (context, state) => BotsScreen( - publisherName: state.pathParameters['name']!, - appId: state.pathParameters['appId']!, - ), - ), - GoRoute( - name: 'developerBotNew', - path: '/developers/:name/bots/new', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - ), - ), - GoRoute( - name: 'developerBotNewApp', - path: '/developers/:name/apps/:appId/bots/new', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - appId: state.pathParameters['appId']!, - ), - ), - GoRoute( - name: 'developerBotEdit', - path: '/developers/:name/bots/:id', - builder: - (context, state) => EditBotScreen( + (context, state) => EditProjectScreen( publisherName: state.pathParameters['name']!, id: state.pathParameters['id']!, ), ), GoRoute( - name: 'developerBotEditApp', - path: '/developers/:name/apps/:appId/bots/:id', + name: 'developerProjectDetail', + path: '/developers/:name/projects/:projectId', builder: - (context, state) => EditBotScreen( + (context, state) => ProjectDetailScreen( publisherName: state.pathParameters['name']!, - id: state.pathParameters['id']!, - appId: state.pathParameters['appId']!, - ), - ), - GoRoute( - name: 'developerBotDetail', - path: '/developers/:name/bots/:id/detail', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - id: state.pathParameters['id']!, - ), - ), - GoRoute( - name: 'developerBotDetailApp', - path: '/developers/:name/apps/:appId/bots/:id/detail', - builder: - (context, state) => EditBotScreen( - publisherName: state.pathParameters['name']!, - id: state.pathParameters['id']!, - appId: state.pathParameters['appId']!, + projectId: state.pathParameters['projectId']!, ), + routes: [ + GoRoute( + name: 'developerAppNew', + path: 'apps/new', + builder: + (context, state) => NewCustomAppScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + ), + ), + GoRoute( + name: 'developerAppEdit', + path: 'apps/:id/edit', + builder: + (context, state) => EditAppScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + id: state.pathParameters['id']!, + ), + ), + GoRoute( + name: 'developerBotNew', + path: 'bots/new', + builder: + (context, state) => EditBotScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + ), + ), + GoRoute( + name: 'developerBotEdit', + path: 'bots/:id/edit', + builder: + (context, state) => EditBotScreen( + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + id: state.pathParameters['id']!, + ), + ), + GoRoute( + name: 'developerBotDetail', + path: 'bots/:id/detail', + builder: + (context, state) => EditBotScreen( + // Assuming EditBotScreen can also serve as a detail view + publisherName: state.pathParameters['name']!, + projectId: state.pathParameters['projectId']!, + id: state.pathParameters['id']!, + ), + ), + ], ), ], ), diff --git a/lib/screens/developers/apps.dart b/lib/screens/developers/apps.dart index 5cc07638..0d18c8db 100644 --- a/lib/screens/developers/apps.dart +++ b/lib/screens/developers/apps.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/custom_app.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; -import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -16,152 +15,179 @@ import 'package:styled_widget/styled_widget.dart'; part 'apps.g.dart'; @riverpod -Future> customApps(Ref ref, String publisherName) async { +Future> customApps( + Ref ref, + String publisherName, + String projectId, +) async { final client = ref.watch(apiClientProvider); - final resp = await client.get('/develop/$publisherName'); - return resp.data.map((e) => CustomApp.fromJson(e)).cast().toList(); + final resp = await client.get( + '/develop/developers/$publisherName/projects/$projectId/apps', + ); + return (resp.data as List) + .map((e) => CustomApp.fromJson(e)) + .cast() + .toList(); } class CustomAppsScreen extends HookConsumerWidget { final String publisherName; - const CustomAppsScreen({super.key, required this.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)); + final apps = ref.watch(customAppsProvider(publisherName, projectId)); - return AppScaffold( - appBar: AppBar( - title: Text('customApps').tr(), - actions: [ - IconButton( - icon: const Icon(Symbols.add), - onPressed: () { - context.pushNamed( - 'developerAppNew', - pathParameters: {'name': publisherName}, + 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: () { + context.pushNamed( + 'developerAppNew', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + }, + ); + }, + icon: const Icon(Symbols.add), + label: Text('createCustomApp').tr(), + ), + ], + ), + ); + } + return RefreshIndicator( + onRefresh: + () => ref.refresh( + customAppsProvider(publisherName, projectId).future, + ), + child: ListView.builder( + padding: EdgeInsets.only(top: 4), + itemCount: data.length, + itemBuilder: (context, index) { + final app = data[index]; + return Card( + margin: const EdgeInsets.all(8.0), + child: Column( + children: [ + SizedBox( + height: 150, + child: Stack( + fit: StackFit.expand, + children: [ + if (app.background != null) + CloudFileWidget( + item: app.background!, + fit: BoxFit.cover, + ).clipRRect(topLeft: 8, topRight: 8), + if (app.picture != null) + Positioned( + left: 16, + bottom: 16, + child: ProfilePictureWidget( + fileId: app.picture!.id, + radius: 40, + fallbackIcon: Symbols.apps, + ), + ), + ], + ), + ), + ListTile( + title: Text(app.name), + 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') { + context.pushNamed( + 'developerAppEdit', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + 'id': app.id, + }, + ); + } else if (value == 'delete') { + showConfirmAlert( + 'deleteCustomAppHint'.tr(), + 'deleteCustomApp'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.read(apiClientProvider); + client.delete( + '/develop/developers/$publisherName/projects/$projectId/apps/${app.id}', + ); + ref.invalidate( + customAppsProvider(publisherName, projectId), + ); + } + }); + } + }, + ), + ), + ], + ), ); }, ), - ], - ), - body: apps.when( - data: (data) { - if (data.isEmpty) { - return Center(child: Text('noCustomApps').tr()); - } - return RefreshIndicator( - onRefresh: - () => ref.refresh(customAppsProvider(publisherName).future), - child: ListView.builder( - padding: EdgeInsets.only(top: 4), - itemCount: data.length, - itemBuilder: (context, index) { - final app = data[index]; - return Card( - margin: const EdgeInsets.all(8.0), - child: Column( - children: [ - SizedBox( - height: 150, - child: Stack( - fit: StackFit.expand, - children: [ - if (app.background != null) - CloudFileWidget( - item: app.background!, - fit: BoxFit.cover, - ).clipRRect(topLeft: 8, topRight: 8), - if (app.picture != null) - Positioned( - left: 16, - bottom: 16, - child: ProfilePictureWidget( - fileId: app.picture!.id, - radius: 40, - fallbackIcon: Symbols.apps, - ), - ), - ], - ), - ), - ListTile( - title: Text(app.name), - 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') { - context.pushNamed( - 'developerAppEdit', - pathParameters: { - 'name': publisherName, - 'id': app.id, - }, - ); - } else if (value == 'delete') { - showConfirmAlert( - 'deleteCustomAppHint'.tr(), - 'deleteCustomApp'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.read(apiClientProvider); - client.delete('/develop/apps/${app.id}'); - ref.invalidate( - customAppsProvider(publisherName), - ); - } - }); - } - }, - ), - ), - ], - ), - ); - }, - ), - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: - (err, stack) => ResponseErrorWidget( - error: err, - onRetry: () => ref.invalidate(customAppsProvider(publisherName)), - ), - ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => ResponseErrorWidget( + error: err, + onRetry: + () => ref.invalidate( + customAppsProvider(publisherName, projectId), + ), + ), ); } } diff --git a/lib/screens/developers/apps.g.dart b/lib/screens/developers/apps.g.dart index 850759be..de6654ac 100644 --- a/lib/screens/developers/apps.g.dart +++ b/lib/screens/developers/apps.g.dart @@ -6,7 +6,7 @@ part of 'apps.dart'; // RiverpodGenerator // ************************************************************************** -String _$customAppsHash() => r'bcceb50ddbc9ca01f6555faf9b4f9ed21a7b5057'; +String _$customAppsHash() => r'c36e5ee59f16a29220dc0e9fba65e579d341a28f'; /// Copied from Dart SDK class _SystemHash { @@ -39,15 +39,15 @@ class CustomAppsFamily extends Family>> { const CustomAppsFamily(); /// See also [customApps]. - CustomAppsProvider call(String publisherName) { - return CustomAppsProvider(publisherName); + CustomAppsProvider call(String publisherName, String projectId) { + return CustomAppsProvider(publisherName, projectId); } @override CustomAppsProvider getProviderOverride( covariant CustomAppsProvider provider, ) { - return call(provider.publisherName); + return call(provider.publisherName, provider.projectId); } static const Iterable? _dependencies = null; @@ -68,9 +68,9 @@ class CustomAppsFamily extends Family>> { /// See also [customApps]. class CustomAppsProvider extends AutoDisposeFutureProvider> { /// See also [customApps]. - CustomAppsProvider(String publisherName) + CustomAppsProvider(String publisherName, String projectId) : this._internal( - (ref) => customApps(ref as CustomAppsRef, publisherName), + (ref) => customApps(ref as CustomAppsRef, publisherName, projectId), from: customAppsProvider, name: r'customAppsProvider', debugGetCreateSourceHash: @@ -80,6 +80,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider> { dependencies: CustomAppsFamily._dependencies, allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies, publisherName: publisherName, + projectId: projectId, ); CustomAppsProvider._internal( @@ -90,9 +91,11 @@ class CustomAppsProvider extends AutoDisposeFutureProvider> { required super.debugGetCreateSourceHash, required super.from, required this.publisherName, + required this.projectId, }) : super.internal(); final String publisherName; + final String projectId; @override Override overrideWith( @@ -108,6 +111,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider> { allTransitiveDependencies: null, debugGetCreateSourceHash: null, publisherName: publisherName, + projectId: projectId, ), ); } @@ -119,13 +123,16 @@ class CustomAppsProvider extends AutoDisposeFutureProvider> { @override bool operator ==(Object other) { - return other is CustomAppsProvider && other.publisherName == publisherName; + return other is CustomAppsProvider && + other.publisherName == publisherName && + other.projectId == projectId; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); return _SystemHash.finish(hash); } @@ -136,6 +143,9 @@ class CustomAppsProvider extends AutoDisposeFutureProvider> { mixin CustomAppsRef on AutoDisposeFutureProviderRef> { /// The parameter `publisherName` of this provider. String get publisherName; + + /// The parameter `projectId` of this provider. + String get projectId; } class _CustomAppsProviderElement @@ -145,6 +155,8 @@ class _CustomAppsProviderElement @override String get publisherName => (origin as CustomAppsProvider).publisherName; + @override + String get projectId => (origin as CustomAppsProvider).projectId; } // ignore_for_file: type=lint diff --git a/lib/screens/developers/bots.dart b/lib/screens/developers/bots.dart index bc6a66d1..8c5bf213 100644 --- a/lib/screens/developers/bots.dart +++ b/lib/screens/developers/bots.dart @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/bot.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; -import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -14,147 +13,150 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'bots.g.dart'; @riverpod -Future> bots(Ref ref, String publisherName, {String? appId}) async { +Future> bots(Ref ref, String publisherName, String projectId) async { final client = ref.watch(apiClientProvider); - final queryParams = { - 'publisher': publisherName, - if (appId != null) 'app_id': appId, - }; - final resp = await client.get('/develop/bots', queryParameters: queryParams); - return resp.data.map((e) => Bot.fromJson(e)).cast().toList(); + final resp = await client.get( + '/develop/developers/$publisherName/projects/$projectId/bots', + ); + return (resp.data as List).map((e) => Bot.fromJson(e)).cast().toList(); } class BotsScreen extends HookConsumerWidget { final String publisherName; - final String? appId; - const BotsScreen({super.key, required this.publisherName, this.appId}); + 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, appId: appId)); + final botsList = ref.watch(botsProvider(publisherName, projectId)); - return AppScaffold( - appBar: AppBar( - title: Text('bots').tr(), - actions: [ - IconButton( - icon: const Icon(Symbols.add), - onPressed: () { - context.pushNamed( - 'developerBotNew', - pathParameters: { - 'name': publisherName, - if (appId != null) 'appId': appId!, - }, + 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: () { + context.pushNamed( + 'developerBotNew', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + }, + ); + }, + icon: const Icon(Symbols.add), + label: Text('createBot').tr(), + ), + ], + ), + ); + } + return RefreshIndicator( + onRefresh: + () => ref.refresh(botsProvider(publisherName, projectId).future), + child: ListView.builder( + padding: const EdgeInsets.only(top: 4), + itemCount: data.length, + itemBuilder: (context, index) { + final bot = data[index]; + return Card( + margin: const EdgeInsets.all(8.0), + child: ListTile( + leading: CircleAvatar( + child: + bot.picture != null + ? CloudFileWidget(item: bot.picture!) + : const Icon(Symbols.smart_toy), + ), + title: Text(bot.name), + subtitle: Text(bot.description ?? ''), + 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') { + context.pushNamed( + 'developerBotEdit', + pathParameters: { + 'name': publisherName, + 'projectId': projectId, + 'id': bot.id, + }, + ); + } else if (value == 'delete') { + showConfirmAlert( + 'deleteBotHint'.tr(), + 'deleteBot'.tr(), + ).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, + 'id': bot.id, + }, + ); + }, + ), ); }, ), - ], - ), - body: botsList.when( - data: (data) { - if (data.isEmpty) { - return Center(child: Text('noBots').tr()); - } - return RefreshIndicator( - onRefresh: - () => ref.refresh( - botsProvider(publisherName, appId: appId).future, - ), - child: ListView.builder( - padding: const EdgeInsets.only(top: 4), - itemCount: data.length, - itemBuilder: (context, index) { - final bot = data[index]; - return Card( - margin: const EdgeInsets.all(8.0), - child: ListTile( - leading: CircleAvatar( - child: - bot.picture != null - ? CloudFileWidget(item: bot.picture!) - : const Icon(Symbols.smart_toy), - ), - title: Text(bot.name), - subtitle: Text(bot.description ?? ''), - 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') { - context.pushNamed( - 'developerBotEdit', - pathParameters: { - 'name': publisherName, - 'id': bot.id, - if (appId != null) 'appId': appId!, - }, - ); - } else if (value == 'delete') { - showConfirmAlert( - 'deleteBotHint'.tr(), - 'deleteBot'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.read(apiClientProvider); - client.delete('/develop/bots/${bot.id}'); - ref.invalidate( - botsProvider(publisherName, appId: appId), - ); - } - }); - } - }, - ), - onTap: () { - context.pushNamed( - 'developerBotDetail', - pathParameters: { - 'name': publisherName, - 'id': bot.id, - if (appId != null) 'appId': appId!, - }, - ); - }, - ), - ); - }, - ), - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: - (err, stack) => ResponseErrorWidget( - error: err, - onRetry: - () => - ref.invalidate(botsProvider(publisherName, appId: appId)), - ), - ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => ResponseErrorWidget( + error: err, + onRetry: + () => ref.invalidate(botsProvider(publisherName, projectId)), + ), ); } } diff --git a/lib/screens/developers/bots.g.dart b/lib/screens/developers/bots.g.dart index 4f9f72f6..e4e8f9f9 100644 --- a/lib/screens/developers/bots.g.dart +++ b/lib/screens/developers/bots.g.dart @@ -6,7 +6,7 @@ part of 'bots.dart'; // RiverpodGenerator // ************************************************************************** -String _$botsHash() => r'04bff237afa91032310eaa8acd792c5a98da0d75'; +String _$botsHash() => r'a54c8b4df23f94754398706779044903fcca6eea'; /// Copied from Dart SDK class _SystemHash { @@ -39,13 +39,13 @@ class BotsFamily extends Family>> { const BotsFamily(); /// See also [bots]. - BotsProvider call(String publisherName, {String? appId}) { - return BotsProvider(publisherName, appId: appId); + BotsProvider call(String publisherName, String projectId) { + return BotsProvider(publisherName, projectId); } @override BotsProvider getProviderOverride(covariant BotsProvider provider) { - return call(provider.publisherName, appId: provider.appId); + return call(provider.publisherName, provider.projectId); } static const Iterable? _dependencies = null; @@ -66,9 +66,9 @@ class BotsFamily extends Family>> { /// See also [bots]. class BotsProvider extends AutoDisposeFutureProvider> { /// See also [bots]. - BotsProvider(String publisherName, {String? appId}) + BotsProvider(String publisherName, String projectId) : this._internal( - (ref) => bots(ref as BotsRef, publisherName, appId: appId), + (ref) => bots(ref as BotsRef, publisherName, projectId), from: botsProvider, name: r'botsProvider', debugGetCreateSourceHash: @@ -76,7 +76,7 @@ class BotsProvider extends AutoDisposeFutureProvider> { dependencies: BotsFamily._dependencies, allTransitiveDependencies: BotsFamily._allTransitiveDependencies, publisherName: publisherName, - appId: appId, + projectId: projectId, ); BotsProvider._internal( @@ -87,11 +87,11 @@ class BotsProvider extends AutoDisposeFutureProvider> { required super.debugGetCreateSourceHash, required super.from, required this.publisherName, - required this.appId, + required this.projectId, }) : super.internal(); final String publisherName; - final String? appId; + final String projectId; @override Override overrideWith(FutureOr> Function(BotsRef provider) create) { @@ -105,7 +105,7 @@ class BotsProvider extends AutoDisposeFutureProvider> { allTransitiveDependencies: null, debugGetCreateSourceHash: null, publisherName: publisherName, - appId: appId, + projectId: projectId, ), ); } @@ -119,14 +119,14 @@ class BotsProvider extends AutoDisposeFutureProvider> { bool operator ==(Object other) { return other is BotsProvider && other.publisherName == publisherName && - other.appId == appId; + other.projectId == projectId; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode); - hash = _SystemHash.combine(hash, appId.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); return _SystemHash.finish(hash); } @@ -138,8 +138,8 @@ mixin BotsRef on AutoDisposeFutureProviderRef> { /// The parameter `publisherName` of this provider. String get publisherName; - /// The parameter `appId` of this provider. - String? get appId; + /// The parameter `projectId` of this provider. + String get projectId; } class _BotsProviderElement extends AutoDisposeFutureProviderElement> @@ -149,7 +149,7 @@ class _BotsProviderElement extends AutoDisposeFutureProviderElement> @override String get publisherName => (origin as BotsProvider).publisherName; @override - String? get appId => (origin as BotsProvider).appId; + String get projectId => (origin as BotsProvider).projectId; } // ignore_for_file: type=lint diff --git a/lib/screens/developers/edit_app.dart b/lib/screens/developers/edit_app.dart index 613de185..b00c1d7e 100644 --- a/lib/screens/developers/edit_app.dart +++ b/lib/screens/developers/edit_app.dart @@ -22,21 +22,37 @@ import 'package:island/widgets/content/sheet.dart'; part 'edit_app.g.dart'; @riverpod -Future customApp(Ref ref, String publisherName, String id) async { +Future customApp( + Ref ref, + String publisherName, + String projectId, + String id, +) async { final client = ref.watch(apiClientProvider); - final resp = await client.get('/develop/apps/$id'); + 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; - const EditAppScreen({super.key, required this.publisherName, this.id}); + const EditAppScreen({ + super.key, + required this.publisherName, + required this.projectId, + this.id, + }); @override Widget build(BuildContext context, WidgetRef ref) { final isNew = id == null; - final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!)); + final app = + isNew + ? null + : ref.watch(customAppProvider(publisherName, projectId, id!)); final formKey = useMemoized(() => GlobalKey()); @@ -283,13 +299,16 @@ class EditAppScreen extends HookConsumerWidget { }; if (isNew) { await client.post( - '/develop/apps', - data: {...data, 'publisher_id': publisherName}, + '/develop/developers/$publisherName/projects/$projectId/apps', + data: data, ); } else { - await client.patch('/develop/apps/$id', data: data); + await client.patch( + '/develop/developers/$publisherName/projects/$projectId/apps/$id', + data: data, + ); } - ref.invalidate(customAppsProvider(publisherName)); + ref.invalidate(customAppsProvider(publisherName, projectId)); if (context.mounted) { Navigator.pop(context); } @@ -306,7 +325,9 @@ class EditAppScreen extends HookConsumerWidget { ? ResponseErrorWidget( error: app!.error, onRetry: - () => ref.invalidate(customAppProvider(publisherName, id!)), + () => ref.invalidate( + customAppProvider(publisherName, projectId, id!), + ), ) : SingleChildScrollView( child: Column( diff --git a/lib/screens/developers/edit_app.g.dart b/lib/screens/developers/edit_app.g.dart index 56345991..f6f6c2b0 100644 --- a/lib/screens/developers/edit_app.g.dart +++ b/lib/screens/developers/edit_app.g.dart @@ -6,7 +6,7 @@ part of 'edit_app.dart'; // RiverpodGenerator // ************************************************************************** -String _$customAppHash() => r'e2b022c9103cf459f7d81018e34d8f7a31b5c864'; +String _$customAppHash() => r'17b3d1385e59bc5ee7f13fb0f11c56cf8a9ba41f'; /// Copied from Dart SDK class _SystemHash { @@ -39,13 +39,13 @@ class CustomAppFamily extends Family> { const CustomAppFamily(); /// See also [customApp]. - CustomAppProvider call(String publisherName, String id) { - return CustomAppProvider(publisherName, id); + CustomAppProvider call(String publisherName, String projectId, String id) { + return CustomAppProvider(publisherName, projectId, id); } @override CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) { - return call(provider.publisherName, provider.id); + return call(provider.publisherName, provider.projectId, provider.id); } static const Iterable? _dependencies = null; @@ -66,9 +66,9 @@ class CustomAppFamily extends Family> { /// See also [customApp]. class CustomAppProvider extends AutoDisposeFutureProvider { /// See also [customApp]. - CustomAppProvider(String publisherName, String id) + CustomAppProvider(String publisherName, String projectId, String id) : this._internal( - (ref) => customApp(ref as CustomAppRef, publisherName, id), + (ref) => customApp(ref as CustomAppRef, publisherName, projectId, id), from: customAppProvider, name: r'customAppProvider', debugGetCreateSourceHash: @@ -78,6 +78,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider { dependencies: CustomAppFamily._dependencies, allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies, publisherName: publisherName, + projectId: projectId, id: id, ); @@ -89,10 +90,12 @@ class CustomAppProvider extends AutoDisposeFutureProvider { required super.debugGetCreateSourceHash, required super.from, required this.publisherName, + required this.projectId, required this.id, }) : super.internal(); final String publisherName; + final String projectId; final String id; @override @@ -109,6 +112,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider { allTransitiveDependencies: null, debugGetCreateSourceHash: null, publisherName: publisherName, + projectId: projectId, id: id, ), ); @@ -123,6 +127,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider { bool operator ==(Object other) { return other is CustomAppProvider && other.publisherName == publisherName && + other.projectId == projectId && other.id == id; } @@ -130,6 +135,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider { int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); hash = _SystemHash.combine(hash, id.hashCode); return _SystemHash.finish(hash); @@ -142,6 +148,9 @@ mixin CustomAppRef on AutoDisposeFutureProviderRef { /// The parameter `publisherName` of this provider. String get publisherName; + /// The parameter `projectId` of this provider. + String get projectId; + /// The parameter `id` of this provider. String get id; } @@ -154,6 +163,8 @@ class _CustomAppProviderElement @override String get publisherName => (origin as CustomAppProvider).publisherName; @override + String get projectId => (origin as CustomAppProvider).projectId; + @override String get id => (origin as CustomAppProvider).id; } diff --git a/lib/screens/developers/edit_bot.dart b/lib/screens/developers/edit_bot.dart index bc692983..d2c51174 100644 --- a/lib/screens/developers/edit_bot.dart +++ b/lib/screens/developers/edit_bot.dart @@ -2,6 +2,7 @@ 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/models/bot.dart'; @@ -20,27 +21,35 @@ import 'package:styled_widget/styled_widget.dart'; part 'edit_bot.g.dart'; @riverpod -Future bot(Ref ref, String id) async { +Future bot( + Ref ref, + String publisherName, + String projectId, + String id, +) async { final client = ref.watch(apiClientProvider); - final resp = await client.get('/develop/bots/$id'); + 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 String? appId; const EditBotScreen({ super.key, required this.publisherName, + required this.projectId, this.id, - this.appId, }); @override Widget build(BuildContext context, WidgetRef ref) { final isNew = id == null; - final botData = isNew ? null : ref.watch(botProvider(id!)); + final botData = + isNew ? null : ref.watch(botProvider(publisherName, projectId, id!)); final formKey = useMemoized(() => GlobalKey()); final submitting = useState(false); @@ -141,18 +150,22 @@ class EditBotScreen extends HookConsumerWidget { ? documentationController.text : null, }, - 'publisher_id': publisherName, - if (appId != null) 'app_id': appId, }; if (isNew) { - await client.post('/develop/bots', data: data); + await client.post( + '/develop/developers/$publisherName/projects/$projectId/bots', + data: data, + ); } else { - await client.patch('/develop/bots/$id', data: data); + await client.patch( + '/develop/developers/$publisherName/projects/$projectId/bots/$id', + data: data, + ); } if (context.mounted) { - Navigator.pop(context); + context.pop(); } } @@ -164,7 +177,10 @@ class EditBotScreen extends HookConsumerWidget { : botData?.hasError == true && !isNew ? ResponseErrorWidget( error: botData!.error, - onRetry: () => ref.invalidate(botProvider(id!)), + onRetry: + () => ref.invalidate( + botProvider(publisherName, projectId, id!), + ), ) : SingleChildScrollView( child: Column( diff --git a/lib/screens/developers/edit_bot.g.dart b/lib/screens/developers/edit_bot.g.dart index 4f265b5c..00abb1e8 100644 --- a/lib/screens/developers/edit_bot.g.dart +++ b/lib/screens/developers/edit_bot.g.dart @@ -6,7 +6,7 @@ part of 'edit_bot.dart'; // RiverpodGenerator // ************************************************************************** -String _$botHash() => r'267c75029a194fe180aeaebf12cbb0c1da9b8529'; +String _$botHash() => r'a3e412ed575c513434bc718b7920db1d017111f4'; /// Copied from Dart SDK class _SystemHash { @@ -39,13 +39,13 @@ class BotFamily extends Family> { const BotFamily(); /// See also [bot]. - BotProvider call(String id) { - return BotProvider(id); + BotProvider call(String publisherName, String projectId, String id) { + return BotProvider(publisherName, projectId, id); } @override BotProvider getProviderOverride(covariant BotProvider provider) { - return call(provider.id); + return call(provider.publisherName, provider.projectId, provider.id); } static const Iterable? _dependencies = null; @@ -66,15 +66,17 @@ class BotFamily extends Family> { /// See also [bot]. class BotProvider extends AutoDisposeFutureProvider { /// See also [bot]. - BotProvider(String id) + BotProvider(String publisherName, String projectId, String id) : this._internal( - (ref) => bot(ref as BotRef, id), + (ref) => bot(ref as BotRef, publisherName, projectId, id), from: botProvider, name: r'botProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$botHash, dependencies: BotFamily._dependencies, allTransitiveDependencies: BotFamily._allTransitiveDependencies, + publisherName: publisherName, + projectId: projectId, id: id, ); @@ -85,9 +87,13 @@ class BotProvider extends AutoDisposeFutureProvider { required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, + required this.publisherName, + required this.projectId, required this.id, }) : super.internal(); + final String publisherName; + final String projectId; final String id; @override @@ -101,6 +107,8 @@ class BotProvider extends AutoDisposeFutureProvider { dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, + publisherName: publisherName, + projectId: projectId, id: id, ), ); @@ -113,12 +121,17 @@ class BotProvider extends AutoDisposeFutureProvider { @override bool operator ==(Object other) { - return other is BotProvider && other.id == id; + return other is BotProvider && + other.publisherName == publisherName && + other.projectId == projectId && + other.id == id; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, publisherName.hashCode); + hash = _SystemHash.combine(hash, projectId.hashCode); hash = _SystemHash.combine(hash, id.hashCode); return _SystemHash.finish(hash); @@ -128,6 +141,12 @@ class BotProvider extends AutoDisposeFutureProvider { @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element mixin BotRef on AutoDisposeFutureProviderRef { + /// The parameter `publisherName` of this provider. + String get publisherName; + + /// The parameter `projectId` of this provider. + String get projectId; + /// The parameter `id` of this provider. String get id; } @@ -136,6 +155,10 @@ class _BotProviderElement extends AutoDisposeFutureProviderElement with BotRef { _BotProviderElement(super.provider); + @override + String get publisherName => (origin as BotProvider).publisherName; + @override + String get projectId => (origin as BotProvider).projectId; @override String get id => (origin as BotProvider).id; } diff --git a/lib/screens/developers/edit_project.dart b/lib/screens/developers/edit_project.dart new file mode 100644 index 00000000..12d888a8 --- /dev/null +++ b/lib/screens/developers/edit_project.dart @@ -0,0 +1,130 @@ +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/models/dev_project.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/screens/developers/projects.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/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(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()); + 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), + ), + ), + ); + } +} diff --git a/lib/screens/developers/edit_project.g.dart b/lib/screens/developers/edit_project.g.dart new file mode 100644 index 00000000..ae900733 --- /dev/null +++ b/lib/screens/developers/edit_project.g.dart @@ -0,0 +1,163 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'edit_project.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$devProjectHash() => r'fc68254c6e598e3fa05c86c36f1469c0b689bc43'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [devProject]. +@ProviderFor(devProject) +const devProjectProvider = DevProjectFamily(); + +/// See also [devProject]. +class DevProjectFamily extends Family> { + /// See also [devProject]. + const DevProjectFamily(); + + /// See also [devProject]. + DevProjectProvider call(String pubName, String id) { + return DevProjectProvider(pubName, id); + } + + @override + DevProjectProvider getProviderOverride( + covariant DevProjectProvider provider, + ) { + return call(provider.pubName, provider.id); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'devProjectProvider'; +} + +/// See also [devProject]. +class DevProjectProvider extends AutoDisposeFutureProvider { + /// See also [devProject]. + DevProjectProvider(String pubName, String id) + : this._internal( + (ref) => devProject(ref as DevProjectRef, pubName, id), + from: devProjectProvider, + name: r'devProjectProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$devProjectHash, + dependencies: DevProjectFamily._dependencies, + allTransitiveDependencies: DevProjectFamily._allTransitiveDependencies, + pubName: pubName, + id: id, + ); + + DevProjectProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.pubName, + required this.id, + }) : super.internal(); + + final String pubName; + final String id; + + @override + Override overrideWith( + FutureOr Function(DevProjectRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DevProjectProvider._internal( + (ref) => create(ref as DevProjectRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + pubName: pubName, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _DevProjectProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DevProjectProvider && + other.pubName == pubName && + other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, pubName.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DevProjectRef on AutoDisposeFutureProviderRef { + /// The parameter `pubName` of this provider. + String get pubName; + + /// The parameter `id` of this provider. + String get id; +} + +class _DevProjectProviderElement + extends AutoDisposeFutureProviderElement + with DevProjectRef { + _DevProjectProviderElement(super.provider); + + @override + String get pubName => (origin as DevProjectProvider).pubName; + @override + String get id => (origin as DevProjectProvider).id; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/developers/hub.dart b/lib/screens/developers/hub.dart index 8d535151..ef19cca3 100644 --- a/lib/screens/developers/hub.dart +++ b/lib/screens/developers/hub.dart @@ -235,33 +235,15 @@ class DeveloperHubScreen extends HookConsumerWidget { ).padding(vertical: 12, horizontal: 12), ListTile( minTileHeight: 48, - title: Text('customApps').tr(), - trailing: Icon(Symbols.chevron_right), - leading: const Icon(Symbols.apps), - contentPadding: EdgeInsets.symmetric( + title: Text('projects').tr(), + trailing: const Icon(Symbols.chevron_right), + leading: const Icon(Symbols.folder_managed), + contentPadding: const EdgeInsets.symmetric( horizontal: 24, ), onTap: () { context.pushNamed( - 'developerApps', - pathParameters: { - 'name': - currentDeveloper.value!.publisher!.name, - }, - ); - }, - ), - ListTile( - minTileHeight: 48, - title: Text('bots').tr(), - trailing: Icon(Symbols.chevron_right), - leading: const Icon(Symbols.smart_toy), - contentPadding: EdgeInsets.symmetric( - horizontal: 24, - ), - onTap: () { - context.pushNamed( - 'developerBots', + 'developerProjects', pathParameters: { 'name': currentDeveloper.value!.publisher!.name, diff --git a/lib/screens/developers/hub.g.dart b/lib/screens/developers/hub.g.dart index 791e3c64..d287526a 100644 --- a/lib/screens/developers/hub.g.dart +++ b/lib/screens/developers/hub.g.dart @@ -6,7 +6,7 @@ part of 'hub.dart'; // RiverpodGenerator // ************************************************************************** -String _$developerStatsHash() => r'4ca5c3f7abf4158cb32116e806f18faa888020d5'; +String _$developerStatsHash() => r'45546f29ec7cd1a9c3a4e0f4e39275e78bf34755'; /// Copied from Dart SDK class _SystemHash { @@ -149,7 +149,7 @@ class _DeveloperStatsProviderElement String? get uname => (origin as DeveloperStatsProvider).uname; } -String _$developersHash() => r'1793a1897ad105cb424525b357fd33ed15215f26'; +String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2'; /// See also [developers]. @ProviderFor(developers) diff --git a/lib/screens/developers/new_app.dart b/lib/screens/developers/new_app.dart index d55b98d4..7d42cce4 100644 --- a/lib/screens/developers/new_app.dart +++ b/lib/screens/developers/new_app.dart @@ -3,10 +3,11 @@ import 'package:island/screens/developers/edit_app.dart'; class NewCustomAppScreen extends StatelessWidget { final String publisherName; - const NewCustomAppScreen({super.key, required this.publisherName}); + final String projectId; + const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId}); @override Widget build(BuildContext context) { - return EditAppScreen(publisherName: publisherName); + return EditAppScreen(publisherName: publisherName, projectId: projectId); } } diff --git a/lib/screens/developers/new_project.dart b/lib/screens/developers/new_project.dart new file mode 100644 index 00000000..00884059 --- /dev/null +++ b/lib/screens/developers/new_project.dart @@ -0,0 +1,13 @@ + +import 'package:flutter/material.dart'; +import 'package:island/screens/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); + } +} diff --git a/lib/screens/developers/project_detail.dart b/lib/screens/developers/project_detail.dart new file mode 100644 index 00000000..abb5008b --- /dev/null +++ b/lib/screens/developers/project_detail.dart @@ -0,0 +1,71 @@ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/screens/developers/apps.dart'; +import 'package:island/screens/developers/bots.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class ProjectDetailScreen extends HookConsumerWidget { + final String publisherName; + final String projectId; + + const ProjectDetailScreen({ + super.key, + required this.publisherName, + required this.projectId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DefaultTabController( + length: 2, + child: AppScaffold( + appBar: AppBar( + title: Text('projectDetails').tr(), + actions: [ + IconButton( + icon: const Icon(Symbols.add), + onPressed: () { + // Get current tab index + final tabController = DefaultTabController.of(context); + final index = tabController.index; + if (index == 0) { + context.pushNamed( + 'developerAppNew', + pathParameters: { + 'name': publisherName, + 'projectId': projectId + }, + ); + } else { + context.pushNamed( + 'developerBotNew', + pathParameters: { + 'name': publisherName, + 'projectId': projectId + }, + ); + } + }, + ), + ], + bottom: TabBar( + tabs: [ + Tab(text: 'customApps'.tr()), + Tab(text: 'bots'.tr()), + ], + ), + ), + body: TabBarView( + children: [ + CustomAppsScreen(publisherName: publisherName, projectId: projectId), + BotsScreen(publisherName: publisherName, projectId: projectId), + ], + ), + ), + ); + } +} diff --git a/lib/screens/developers/projects.dart b/lib/screens/developers/projects.dart new file mode 100644 index 00000000..0455a900 --- /dev/null +++ b/lib/screens/developers/projects.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/dev_project.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/response.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'projects.g.dart'; + +@riverpod +Future> devProjects(Ref ref, String pubName) async { + 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() + .toList(); +} + +class DevProjectsScreen extends HookConsumerWidget { + final String publisherName; + const DevProjectsScreen({super.key, required this.publisherName}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final projects = ref.watch(devProjectsProvider(publisherName)); + + return AppScaffold( + appBar: AppBar( + title: Text('projects').tr(), + actions: [ + IconButton( + icon: const Icon(Symbols.add), + onPressed: () { + context.pushNamed( + 'developerProjectNew', + pathParameters: {'name': publisherName}, + ); + }, + ), + ], + ), + body: projects.when( + data: (data) { + if (data.isEmpty) { + return Center(child: Text('noProjects').tr()); + } + return RefreshIndicator( + onRefresh: + () => ref.refresh(devProjectsProvider(publisherName).future), + child: ListView.builder( + padding: EdgeInsets.only(top: 4), + itemCount: data.length, + itemBuilder: (context, index) { + final project = data[index]; + return Card( + margin: const EdgeInsets.all(8.0), + child: ListTile( + title: Text(project.name), + subtitle: Text(project.description ?? ''), + 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') { + context.pushNamed( + 'developerProjectEdit', + pathParameters: { + 'name': publisherName, + 'id': project.id, + }, + ); + } else if (value == 'delete') { + showConfirmAlert( + 'deleteProjectHint'.tr(), + 'deleteProject'.tr(), + ).then((confirm) { + if (confirm) { + final client = ref.read(apiClientProvider); + client.delete( + '/develop/developers/$publisherName/projects/${project.id}', + ); + ref.invalidate( + devProjectsProvider(publisherName), + ); + } + }); + } + }, + ), + onTap: () { + context.pushNamed( + 'developerProjectDetail', + pathParameters: { + 'name': publisherName, + 'projectId': project.id, + }, + ); + }, + ), + ); + }, + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: + (err, stack) => ResponseErrorWidget( + error: err, + onRetry: () => ref.invalidate(devProjectsProvider(publisherName)), + ), + ), + ); + } +} diff --git a/lib/screens/developers/projects.g.dart b/lib/screens/developers/projects.g.dart new file mode 100644 index 00000000..ad490b91 --- /dev/null +++ b/lib/screens/developers/projects.g.dart @@ -0,0 +1,151 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'projects.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$devProjectsHash() => r'4c86ea5c3c02185514dbfa32804f1529f68d56c7'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [devProjects]. +@ProviderFor(devProjects) +const devProjectsProvider = DevProjectsFamily(); + +/// See also [devProjects]. +class DevProjectsFamily extends Family>> { + /// See also [devProjects]. + const DevProjectsFamily(); + + /// See also [devProjects]. + DevProjectsProvider call(String pubName) { + return DevProjectsProvider(pubName); + } + + @override + DevProjectsProvider getProviderOverride( + covariant DevProjectsProvider provider, + ) { + return call(provider.pubName); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'devProjectsProvider'; +} + +/// See also [devProjects]. +class DevProjectsProvider extends AutoDisposeFutureProvider> { + /// See also [devProjects]. + DevProjectsProvider(String pubName) + : this._internal( + (ref) => devProjects(ref as DevProjectsRef, pubName), + from: devProjectsProvider, + name: r'devProjectsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$devProjectsHash, + dependencies: DevProjectsFamily._dependencies, + allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies, + pubName: pubName, + ); + + DevProjectsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.pubName, + }) : super.internal(); + + final String pubName; + + @override + Override overrideWith( + FutureOr> Function(DevProjectsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: DevProjectsProvider._internal( + (ref) => create(ref as DevProjectsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + pubName: pubName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _DevProjectsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DevProjectsProvider && other.pubName == pubName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, pubName.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin DevProjectsRef on AutoDisposeFutureProviderRef> { + /// The parameter `pubName` of this provider. + String get pubName; +} + +class _DevProjectsProviderElement + extends AutoDisposeFutureProviderElement> + with DevProjectsRef { + _DevProjectsProviderElement(super.provider); + + @override + String get pubName => (origin as DevProjectsProvider).pubName; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/swagger-develop.json b/swagger-develop.json new file mode 100644 index 00000000..1365adff --- /dev/null +++ b/swagger-develop.json @@ -0,0 +1,1169 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Develop API", + "version": "v1" + }, + "paths": { + "/api/developers/{pubName}/projects/{projectId}/bots": { + "get": { + "tags": [ + "BotAccount" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": [ + "BotAccount" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/BotRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/BotRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/developers/{pubName}/projects/{projectId}/bots/{botId}": { + "get": { + "tags": [ + "BotAccount" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "botId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "put": { + "tags": [ + "BotAccount" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "botId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBotRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBotRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateBotRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "BotAccount" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "botId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/developers/{pubName}/projects/{projectId}/apps": { + "get": { + "tags": [ + "CustomApp" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": [ + "CustomApp" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/developers/{pubName}/projects/{projectId}/apps/{appId}": { + "get": { + "tags": [ + "CustomApp" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "patch": { + "tags": [ + "CustomApp" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CustomAppRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "CustomApp" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "appId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/developers/{name}": { + "get": { + "tags": [ + "Developer" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + } + } + } + } + } + }, + "/api/developers/{name}/stats": { + "get": { + "tags": [ + "Developer" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/DeveloperStats" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeveloperStats" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DeveloperStats" + } + } + } + } + } + } + }, + "/api/developers": { + "get": { + "tags": [ + "Developer" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Developer" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Developer" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Developer" + } + } + } + } + } + } + } + }, + "/api/developers/{name}/enroll": { + "post": { + "tags": [ + "Developer" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Developer" + } + } + } + } + } + } + }, + "/api/developers/{pubName}/projects": { + "get": { + "tags": [ + "DevProject" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "tags": [ + "DevProject" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/developers/{pubName}/projects/{id}": { + "get": { + "tags": [ + "DevProject" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "put": { + "tags": [ + "DevProject" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/DevProjectRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "DevProject" + ], + "parameters": [ + { + "name": "pubName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "BotRequest": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CloudFileReferenceObject": { + "type": "object", + "properties": { + "created_at": { + "$ref": "#/components/schemas/Instant" + }, + "updated_at": { + "$ref": "#/components/schemas/Instant" + }, + "deleted_at": { + "$ref": "#/components/schemas/Instant" + }, + "id": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "file_meta": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "nullable": true + }, + "user_meta": { + "type": "object", + "additionalProperties": { + "nullable": true + }, + "nullable": true + }, + "sensitive_marks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentSensitiveMark" + }, + "nullable": true + }, + "mime_type": { + "type": "string", + "nullable": true + }, + "hash": { + "type": "string", + "nullable": true + }, + "size": { + "type": "integer", + "format": "int64" + }, + "has_compression": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ContentSensitiveMark": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12 + ], + "type": "integer", + "format": "int32" + }, + "CustomAppLinks": { + "type": "object", + "properties": { + "home_page": { + "maxLength": 8192, + "type": "string", + "nullable": true + }, + "privacy_policy": { + "maxLength": 8192, + "type": "string", + "nullable": true + }, + "terms_of_service": { + "maxLength": 8192, + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "CustomAppOauthConfig": { + "type": "object", + "properties": { + "client_uri": { + "maxLength": 1024, + "type": "string", + "nullable": true + }, + "redirect_uris": { + "maxItems": 4096, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "post_logout_redirect_uris": { + "maxItems": 4096, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "allowed_scopes": { + "maxItems": 256, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "allowed_grant_types": { + "maxItems": 256, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, + "require_pkce": { + "type": "boolean" + }, + "allow_offline_access": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "CustomAppRequest": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "picture_id": { + "type": "string", + "nullable": true + }, + "background_id": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/CustomAppStatus" + }, + "links": { + "$ref": "#/components/schemas/CustomAppLinks" + }, + "oauth_config": { + "$ref": "#/components/schemas/CustomAppOauthConfig" + } + }, + "additionalProperties": false + }, + "CustomAppStatus": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer", + "format": "int32" + }, + "DevProject": { + "type": "object", + "properties": { + "created_at": { + "$ref": "#/components/schemas/Instant" + }, + "updated_at": { + "$ref": "#/components/schemas/Instant" + }, + "deleted_at": { + "$ref": "#/components/schemas/Instant" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "maxLength": 1024, + "type": "string", + "nullable": true + }, + "name": { + "maxLength": 1024, + "type": "string", + "nullable": true + }, + "description": { + "maxLength": 4096, + "type": "string", + "nullable": true + }, + "developer": { + "$ref": "#/components/schemas/Developer" + }, + "developer_id": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, + "DevProjectRequest": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "Developer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "publisher_id": { + "type": "string", + "format": "uuid" + }, + "projects": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DevProject" + }, + "nullable": true + }, + "publisher": { + "$ref": "#/components/schemas/PublisherInfo" + } + }, + "additionalProperties": false + }, + "DeveloperStats": { + "type": "object", + "properties": { + "total_custom_apps": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "Instant": { + "type": "object", + "additionalProperties": false + }, + "PublisherInfo": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "$ref": "#/components/schemas/PublisherType" + }, + "name": { + "type": "string", + "nullable": true + }, + "nick": { + "type": "string", + "nullable": true + }, + "bio": { + "type": "string", + "nullable": true + }, + "picture": { + "$ref": "#/components/schemas/CloudFileReferenceObject" + }, + "background": { + "$ref": "#/components/schemas/CloudFileReferenceObject" + }, + "verification": { + "$ref": "#/components/schemas/VerificationMark" + }, + "account_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "realm_id": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "PublisherType": { + "enum": [ + 0, + 1, + 2 + ], + "type": "integer", + "format": "int32" + }, + "UpdateBotRequest": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "nullable": true + }, + "is_active": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "VerificationMark": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/VerificationMarkType" + }, + "title": { + "maxLength": 1024, + "type": "string", + "nullable": true + }, + "description": { + "maxLength": 8192, + "type": "string", + "nullable": true + }, + "verified_by": { + "maxLength": 1024, + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "VerificationMarkType": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "type": "integer", + "format": "int32" + } + } + } +} \ No newline at end of file