Add developer projects

This commit is contained in:
2025-08-23 02:56:28 +08:00
parent 7dfe411053
commit 4beda9200e
21 changed files with 2381 additions and 426 deletions

View File

@@ -643,6 +643,14 @@
"enrollDeveloperHint": "Enroll one of your publishers to become a developer.", "enrollDeveloperHint": "Enroll one of your publishers to become a developer.",
"noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.", "noPublishersToEnroll": "You don't have any publishers that can be enrolled as a developer.",
"totalCustomApps": "Total Custom Apps", "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", "customApps": "Custom Apps",
"noCustomApps": "No custom apps yet.", "noCustomApps": "No custom apps yet.",
"createCustomApp": "Create Custom App", "createCustomApp": "Create Custom App",

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

@@ -7,12 +7,14 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/screens/about.dart'; import 'package:island/screens/about.dart';
import 'package:island/screens/account/credits.dart'; import 'package:island/screens/account/credits.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/screens/developers/bots.dart';
import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/edit_app.dart';
import 'package:island/screens/developers/edit_bot.dart'; import 'package:island/screens/developers/edit_bot.dart';
import 'package:island/screens/developers/new_app.dart'; import 'package:island/screens/developers/new_app.dart';
import 'package:island/screens/developers/hub.dart'; import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/developers/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/discovery/articles.dart';
import 'package:island/screens/posts/post_categories_list.dart'; import 'package:island/screens/posts/post_categories_list.dart';
import 'package:island/screens/posts/post_category_detail.dart'; import 'package:island/screens/posts/post_category_detail.dart';
@@ -293,102 +295,89 @@ final routerProvider = Provider<GoRouter>((ref) {
builder: (context, state) => const DeveloperHubScreen(), builder: (context, state) => const DeveloperHubScreen(),
), ),
GoRoute( GoRoute(
name: 'developerApps', name: 'developerProjects',
path: '/developers/:name/apps', path: '/developers/:name/projects',
builder: builder:
(context, state) => CustomAppsScreen( (context, state) => DevProjectsScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
), ),
), ),
GoRoute( GoRoute(
name: 'developerAppNew', name: 'developerProjectNew',
path: '/developers/:name/apps/new', path: '/developers/:name/projects/new',
builder: builder:
(context, state) => NewCustomAppScreen( (context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
), ),
), ),
GoRoute( GoRoute(
name: 'developerAppEdit', name: 'developerProjectEdit',
path: '/developers/:name/apps/:id', path: '/developers/:name/projects/:id/edit',
builder: builder:
(context, state) => EditAppScreen( (context, state) => EditProjectScreen(
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(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!, id: state.pathParameters['id']!,
), ),
), ),
GoRoute( GoRoute(
name: 'developerBotEditApp', name: 'developerProjectDetail',
path: '/developers/:name/apps/:appId/bots/:id', path: '/developers/:name/projects/:projectId',
builder: builder:
(context, state) => EditBotScreen( (context, state) => ProjectDetailScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!, projectId: state.pathParameters['projectId']!,
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']!,
), ),
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']!,
),
),
],
), ),
], ],
), ),

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart'; import 'package:island/models/custom_app.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.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/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -16,152 +15,179 @@ import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart'; part 'apps.g.dart';
@riverpod @riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async { Future<List<CustomApp>> customApps(
Ref ref,
String publisherName,
String projectId,
) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final resp = await client.get('/develop/$publisherName'); final resp = await client.get(
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList(); '/develop/developers/$publisherName/projects/$projectId/apps',
);
return (resp.data as List)
.map((e) => CustomApp.fromJson(e))
.cast<CustomApp>()
.toList();
} }
class CustomAppsScreen extends HookConsumerWidget { class CustomAppsScreen extends HookConsumerWidget {
final String publisherName; final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName}); final String projectId;
const CustomAppsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName)); final apps = ref.watch(customAppsProvider(publisherName, projectId));
return AppScaffold( return apps.when(
appBar: AppBar( data: (data) {
title: Text('customApps').tr(), if (data.isEmpty) {
actions: [ return Center(
IconButton( child: Column(
icon: const Icon(Symbols.add), mainAxisAlignment: MainAxisAlignment.center,
onPressed: () { children: [
context.pushNamed( Text('noCustomApps').tr(),
'developerAppNew', const SizedBox(height: 16),
pathParameters: {'name': publisherName}, 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( loading: () => const Center(child: CircularProgressIndicator()),
data: (data) { error:
if (data.isEmpty) { (err, stack) => ResponseErrorWidget(
return Center(child: Text('noCustomApps').tr()); error: err,
} onRetry:
return RefreshIndicator( () => ref.invalidate(
onRefresh: customAppsProvider(publisherName, projectId),
() => 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)),
),
),
); );
} }
} }

View File

@@ -6,7 +6,7 @@ part of 'apps.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$customAppsHash() => r'bcceb50ddbc9ca01f6555faf9b4f9ed21a7b5057'; String _$customAppsHash() => r'c36e5ee59f16a29220dc0e9fba65e579d341a28f';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -39,15 +39,15 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
const CustomAppsFamily(); const CustomAppsFamily();
/// See also [customApps]. /// See also [customApps].
CustomAppsProvider call(String publisherName) { CustomAppsProvider call(String publisherName, String projectId) {
return CustomAppsProvider(publisherName); return CustomAppsProvider(publisherName, projectId);
} }
@override @override
CustomAppsProvider getProviderOverride( CustomAppsProvider getProviderOverride(
covariant CustomAppsProvider provider, covariant CustomAppsProvider provider,
) { ) {
return call(provider.publisherName); return call(provider.publisherName, provider.projectId);
} }
static const Iterable<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -68,9 +68,9 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps]. /// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> { class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// See also [customApps]. /// See also [customApps].
CustomAppsProvider(String publisherName) CustomAppsProvider(String publisherName, String projectId)
: this._internal( : this._internal(
(ref) => customApps(ref as CustomAppsRef, publisherName), (ref) => customApps(ref as CustomAppsRef, publisherName, projectId),
from: customAppsProvider, from: customAppsProvider,
name: r'customAppsProvider', name: r'customAppsProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -80,6 +80,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
dependencies: CustomAppsFamily._dependencies, dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies, allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName, publisherName: publisherName,
projectId: projectId,
); );
CustomAppsProvider._internal( CustomAppsProvider._internal(
@@ -90,9 +91,11 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.publisherName, required this.publisherName,
required this.projectId,
}) : super.internal(); }) : super.internal();
final String publisherName; final String publisherName;
final String projectId;
@override @override
Override overrideWith( Override overrideWith(
@@ -108,6 +111,7 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
publisherName: publisherName, publisherName: publisherName,
projectId: projectId,
), ),
); );
} }
@@ -119,13 +123,16 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is CustomAppsProvider && other.publisherName == publisherName; return other is CustomAppsProvider &&
other.publisherName == publisherName &&
other.projectId == projectId;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@@ -136,6 +143,9 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> { mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// The parameter `publisherName` of this provider. /// The parameter `publisherName` of this provider.
String get publisherName; String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
} }
class _CustomAppsProviderElement class _CustomAppsProviderElement
@@ -145,6 +155,8 @@ class _CustomAppsProviderElement
@override @override
String get publisherName => (origin as CustomAppsProvider).publisherName; String get publisherName => (origin as CustomAppsProvider).publisherName;
@override
String get projectId => (origin as CustomAppsProvider).projectId;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot.dart'; import 'package:island/models/bot.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.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/content/cloud_files.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@@ -14,147 +13,150 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bots.g.dart'; part 'bots.g.dart';
@riverpod @riverpod
Future<List<Bot>> bots(Ref ref, String publisherName, {String? appId}) async { Future<List<Bot>> bots(Ref ref, String publisherName, String projectId) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final queryParams = { final resp = await client.get(
'publisher': publisherName, '/develop/developers/$publisherName/projects/$projectId/bots',
if (appId != null) 'app_id': appId, );
}; return (resp.data as List).map((e) => Bot.fromJson(e)).cast<Bot>().toList();
final resp = await client.get('/develop/bots', queryParameters: queryParams);
return resp.data.map((e) => Bot.fromJson(e)).cast<Bot>().toList();
} }
class BotsScreen extends HookConsumerWidget { class BotsScreen extends HookConsumerWidget {
final String publisherName; final String publisherName;
final String? appId; final String projectId;
const BotsScreen({super.key, required this.publisherName, this.appId}); const BotsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final botsList = ref.watch(botsProvider(publisherName, appId: appId)); final botsList = ref.watch(botsProvider(publisherName, projectId));
return AppScaffold( return botsList.when(
appBar: AppBar( data: (data) {
title: Text('bots').tr(), if (data.isEmpty) {
actions: [ return Center(
IconButton( child: Column(
icon: const Icon(Symbols.add), mainAxisAlignment: MainAxisAlignment.center,
onPressed: () { children: [
context.pushNamed( Text('noBots').tr(),
'developerBotNew', const SizedBox(height: 16),
pathParameters: { ElevatedButton.icon(
'name': publisherName, onPressed: () {
if (appId != null) 'appId': appId!, 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( loading: () => const Center(child: CircularProgressIndicator()),
data: (data) { error:
if (data.isEmpty) { (err, stack) => ResponseErrorWidget(
return Center(child: Text('noBots').tr()); error: err,
} onRetry:
return RefreshIndicator( () => ref.invalidate(botsProvider(publisherName, projectId)),
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)),
),
),
); );
} }
} }

View File

@@ -6,7 +6,7 @@ part of 'bots.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$botsHash() => r'04bff237afa91032310eaa8acd792c5a98da0d75'; String _$botsHash() => r'a54c8b4df23f94754398706779044903fcca6eea';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -39,13 +39,13 @@ class BotsFamily extends Family<AsyncValue<List<Bot>>> {
const BotsFamily(); const BotsFamily();
/// See also [bots]. /// See also [bots].
BotsProvider call(String publisherName, {String? appId}) { BotsProvider call(String publisherName, String projectId) {
return BotsProvider(publisherName, appId: appId); return BotsProvider(publisherName, projectId);
} }
@override @override
BotsProvider getProviderOverride(covariant BotsProvider provider) { BotsProvider getProviderOverride(covariant BotsProvider provider) {
return call(provider.publisherName, appId: provider.appId); return call(provider.publisherName, provider.projectId);
} }
static const Iterable<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -66,9 +66,9 @@ class BotsFamily extends Family<AsyncValue<List<Bot>>> {
/// See also [bots]. /// See also [bots].
class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> { class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
/// See also [bots]. /// See also [bots].
BotsProvider(String publisherName, {String? appId}) BotsProvider(String publisherName, String projectId)
: this._internal( : this._internal(
(ref) => bots(ref as BotsRef, publisherName, appId: appId), (ref) => bots(ref as BotsRef, publisherName, projectId),
from: botsProvider, from: botsProvider,
name: r'botsProvider', name: r'botsProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -76,7 +76,7 @@ class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
dependencies: BotsFamily._dependencies, dependencies: BotsFamily._dependencies,
allTransitiveDependencies: BotsFamily._allTransitiveDependencies, allTransitiveDependencies: BotsFamily._allTransitiveDependencies,
publisherName: publisherName, publisherName: publisherName,
appId: appId, projectId: projectId,
); );
BotsProvider._internal( BotsProvider._internal(
@@ -87,11 +87,11 @@ class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.publisherName, required this.publisherName,
required this.appId, required this.projectId,
}) : super.internal(); }) : super.internal();
final String publisherName; final String publisherName;
final String? appId; final String projectId;
@override @override
Override overrideWith(FutureOr<List<Bot>> Function(BotsRef provider) create) { Override overrideWith(FutureOr<List<Bot>> Function(BotsRef provider) create) {
@@ -105,7 +105,7 @@ class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
publisherName: publisherName, publisherName: publisherName,
appId: appId, projectId: projectId,
), ),
); );
} }
@@ -119,14 +119,14 @@ class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
bool operator ==(Object other) { bool operator ==(Object other) {
return other is BotsProvider && return other is BotsProvider &&
other.publisherName == publisherName && other.publisherName == publisherName &&
other.appId == appId; other.projectId == projectId;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, appId.hashCode); hash = _SystemHash.combine(hash, projectId.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@@ -138,8 +138,8 @@ mixin BotsRef on AutoDisposeFutureProviderRef<List<Bot>> {
/// The parameter `publisherName` of this provider. /// The parameter `publisherName` of this provider.
String get publisherName; String get publisherName;
/// The parameter `appId` of this provider. /// The parameter `projectId` of this provider.
String? get appId; String get projectId;
} }
class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>> class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>>
@@ -149,7 +149,7 @@ class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>>
@override @override
String get publisherName => (origin as BotsProvider).publisherName; String get publisherName => (origin as BotsProvider).publisherName;
@override @override
String? get appId => (origin as BotsProvider).appId; String get projectId => (origin as BotsProvider).projectId;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint

View File

@@ -22,21 +22,37 @@ import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart'; part 'edit_app.g.dart';
@riverpod @riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async { Future<CustomApp?> customApp(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider); 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); return CustomApp.fromJson(resp.data);
} }
class EditAppScreen extends HookConsumerWidget { class EditAppScreen extends HookConsumerWidget {
final String publisherName; final String publisherName;
final String projectId;
final String? id; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null; 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<FormState>()); final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -283,13 +299,16 @@ class EditAppScreen extends HookConsumerWidget {
}; };
if (isNew) { if (isNew) {
await client.post( await client.post(
'/develop/apps', '/develop/developers/$publisherName/projects/$projectId/apps',
data: {...data, 'publisher_id': publisherName}, data: data,
); );
} else { } 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) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
@@ -306,7 +325,9 @@ class EditAppScreen extends HookConsumerWidget {
? ResponseErrorWidget( ? ResponseErrorWidget(
error: app!.error, error: app!.error,
onRetry: onRetry:
() => ref.invalidate(customAppProvider(publisherName, id!)), () => ref.invalidate(
customAppProvider(publisherName, projectId, id!),
),
) )
: SingleChildScrollView( : SingleChildScrollView(
child: Column( child: Column(

View File

@@ -6,7 +6,7 @@ part of 'edit_app.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$customAppHash() => r'e2b022c9103cf459f7d81018e34d8f7a31b5c864'; String _$customAppHash() => r'17b3d1385e59bc5ee7f13fb0f11c56cf8a9ba41f';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -39,13 +39,13 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
const CustomAppFamily(); const CustomAppFamily();
/// See also [customApp]. /// See also [customApp].
CustomAppProvider call(String publisherName, String id) { CustomAppProvider call(String publisherName, String projectId, String id) {
return CustomAppProvider(publisherName, id); return CustomAppProvider(publisherName, projectId, id);
} }
@override @override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) { CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, provider.id); return call(provider.publisherName, provider.projectId, provider.id);
} }
static const Iterable<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -66,9 +66,9 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
/// See also [customApp]. /// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> { class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// See also [customApp]. /// See also [customApp].
CustomAppProvider(String publisherName, String id) CustomAppProvider(String publisherName, String projectId, String id)
: this._internal( : this._internal(
(ref) => customApp(ref as CustomAppRef, publisherName, id), (ref) => customApp(ref as CustomAppRef, publisherName, projectId, id),
from: customAppProvider, from: customAppProvider,
name: r'customAppProvider', name: r'customAppProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -78,6 +78,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
dependencies: CustomAppFamily._dependencies, dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies, allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName, publisherName: publisherName,
projectId: projectId,
id: id, id: id,
); );
@@ -89,10 +90,12 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.publisherName, required this.publisherName,
required this.projectId,
required this.id, required this.id,
}) : super.internal(); }) : super.internal();
final String publisherName; final String publisherName;
final String projectId;
final String id; final String id;
@override @override
@@ -109,6 +112,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
publisherName: publisherName, publisherName: publisherName,
projectId: projectId,
id: id, id: id,
), ),
); );
@@ -123,6 +127,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
bool operator ==(Object other) { bool operator ==(Object other) {
return other is CustomAppProvider && return other is CustomAppProvider &&
other.publisherName == publisherName && other.publisherName == publisherName &&
other.projectId == projectId &&
other.id == id; other.id == id;
} }
@@ -130,6 +135,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode); hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, projectId.hashCode);
hash = _SystemHash.combine(hash, id.hashCode); hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
@@ -142,6 +148,9 @@ mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp?> {
/// The parameter `publisherName` of this provider. /// The parameter `publisherName` of this provider.
String get publisherName; String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider. /// The parameter `id` of this provider.
String get id; String get id;
} }
@@ -154,6 +163,8 @@ class _CustomAppProviderElement
@override @override
String get publisherName => (origin as CustomAppProvider).publisherName; String get publisherName => (origin as CustomAppProvider).publisherName;
@override @override
String get projectId => (origin as CustomAppProvider).projectId;
@override
String get id => (origin as CustomAppProvider).id; String get id => (origin as CustomAppProvider).id;
} }

View File

@@ -2,6 +2,7 @@ import 'package:croppy/croppy.dart' hide cropImage;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/bot.dart'; import 'package:island/models/bot.dart';
@@ -20,27 +21,35 @@ import 'package:styled_widget/styled_widget.dart';
part 'edit_bot.g.dart'; part 'edit_bot.g.dart';
@riverpod @riverpod
Future<Bot?> bot(Ref ref, String id) async { Future<Bot?> bot(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider); 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); return Bot.fromJson(resp.data);
} }
class EditBotScreen extends HookConsumerWidget { class EditBotScreen extends HookConsumerWidget {
final String publisherName; final String publisherName;
final String projectId;
final String? id; final String? id;
final String? appId;
const EditBotScreen({ const EditBotScreen({
super.key, super.key,
required this.publisherName, required this.publisherName,
required this.projectId,
this.id, this.id,
this.appId,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null; 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<FormState>()); final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false); final submitting = useState(false);
@@ -141,18 +150,22 @@ class EditBotScreen extends HookConsumerWidget {
? documentationController.text ? documentationController.text
: null, : null,
}, },
'publisher_id': publisherName,
if (appId != null) 'app_id': appId,
}; };
if (isNew) { if (isNew) {
await client.post('/develop/bots', data: data); await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots',
data: data,
);
} else { } else {
await client.patch('/develop/bots/$id', data: data); await client.patch(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
data: data,
);
} }
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); context.pop();
} }
} }
@@ -164,7 +177,10 @@ class EditBotScreen extends HookConsumerWidget {
: botData?.hasError == true && !isNew : botData?.hasError == true && !isNew
? ResponseErrorWidget( ? ResponseErrorWidget(
error: botData!.error, error: botData!.error,
onRetry: () => ref.invalidate(botProvider(id!)), onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
) )
: SingleChildScrollView( : SingleChildScrollView(
child: Column( child: Column(

View File

@@ -6,7 +6,7 @@ part of 'edit_bot.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$botHash() => r'267c75029a194fe180aeaebf12cbb0c1da9b8529'; String _$botHash() => r'a3e412ed575c513434bc718b7920db1d017111f4';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -39,13 +39,13 @@ class BotFamily extends Family<AsyncValue<Bot?>> {
const BotFamily(); const BotFamily();
/// See also [bot]. /// See also [bot].
BotProvider call(String id) { BotProvider call(String publisherName, String projectId, String id) {
return BotProvider(id); return BotProvider(publisherName, projectId, id);
} }
@override @override
BotProvider getProviderOverride(covariant BotProvider provider) { BotProvider getProviderOverride(covariant BotProvider provider) {
return call(provider.id); return call(provider.publisherName, provider.projectId, provider.id);
} }
static const Iterable<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -66,15 +66,17 @@ class BotFamily extends Family<AsyncValue<Bot?>> {
/// See also [bot]. /// See also [bot].
class BotProvider extends AutoDisposeFutureProvider<Bot?> { class BotProvider extends AutoDisposeFutureProvider<Bot?> {
/// See also [bot]. /// See also [bot].
BotProvider(String id) BotProvider(String publisherName, String projectId, String id)
: this._internal( : this._internal(
(ref) => bot(ref as BotRef, id), (ref) => bot(ref as BotRef, publisherName, projectId, id),
from: botProvider, from: botProvider,
name: r'botProvider', name: r'botProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botHash, const bool.fromEnvironment('dart.vm.product') ? null : _$botHash,
dependencies: BotFamily._dependencies, dependencies: BotFamily._dependencies,
allTransitiveDependencies: BotFamily._allTransitiveDependencies, allTransitiveDependencies: BotFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id, id: id,
); );
@@ -85,9 +87,13 @@ class BotProvider extends AutoDisposeFutureProvider<Bot?> {
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.publisherName,
required this.projectId,
required this.id, required this.id,
}) : super.internal(); }) : super.internal();
final String publisherName;
final String projectId;
final String id; final String id;
@override @override
@@ -101,6 +107,8 @@ class BotProvider extends AutoDisposeFutureProvider<Bot?> {
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id, id: id,
), ),
); );
@@ -113,12 +121,17 @@ class BotProvider extends AutoDisposeFutureProvider<Bot?> {
@override @override
bool operator ==(Object other) { 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 @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.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); hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
@@ -128,6 +141,12 @@ class BotProvider extends AutoDisposeFutureProvider<Bot?> {
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin BotRef on AutoDisposeFutureProviderRef<Bot?> { mixin BotRef on AutoDisposeFutureProviderRef<Bot?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
/// The parameter `id` of this provider. /// The parameter `id` of this provider.
String get id; String get id;
} }
@@ -136,6 +155,10 @@ class _BotProviderElement extends AutoDisposeFutureProviderElement<Bot?>
with BotRef { with BotRef {
_BotProviderElement(super.provider); _BotProviderElement(super.provider);
@override
String get publisherName => (origin as BotProvider).publisherName;
@override
String get projectId => (origin as BotProvider).projectId;
@override @override
String get id => (origin as BotProvider).id; String get id => (origin as BotProvider).id;
} }

View File

@@ -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?> 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,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<AsyncValue<DevProject?>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'devProjectProvider';
}
/// See also [devProject].
class DevProjectProvider extends AutoDisposeFutureProvider<DevProject?> {
/// 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<DevProject?> 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<DevProject?> 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<DevProject?> {
/// The parameter `pubName` of this provider.
String get pubName;
/// The parameter `id` of this provider.
String get id;
}
class _DevProjectProviderElement
extends AutoDisposeFutureProviderElement<DevProject?>
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

View File

@@ -235,33 +235,15 @@ class DeveloperHubScreen extends HookConsumerWidget {
).padding(vertical: 12, horizontal: 12), ).padding(vertical: 12, horizontal: 12),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: Text('customApps').tr(), title: Text('projects').tr(),
trailing: Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.apps), leading: const Icon(Symbols.folder_managed),
contentPadding: EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
), ),
onTap: () { onTap: () {
context.pushNamed( context.pushNamed(
'developerApps', 'developerProjects',
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',
pathParameters: { pathParameters: {
'name': 'name':
currentDeveloper.value!.publisher!.name, currentDeveloper.value!.publisher!.name,

View File

@@ -6,7 +6,7 @@ part of 'hub.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$developerStatsHash() => r'4ca5c3f7abf4158cb32116e806f18faa888020d5'; String _$developerStatsHash() => r'45546f29ec7cd1a9c3a4e0f4e39275e78bf34755';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -149,7 +149,7 @@ class _DeveloperStatsProviderElement
String? get uname => (origin as DeveloperStatsProvider).uname; String? get uname => (origin as DeveloperStatsProvider).uname;
} }
String _$developersHash() => r'1793a1897ad105cb424525b357fd33ed15215f26'; String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2';
/// See also [developers]. /// See also [developers].
@ProviderFor(developers) @ProviderFor(developers)

View File

@@ -3,10 +3,11 @@ import 'package:island/screens/developers/edit_app.dart';
class NewCustomAppScreen extends StatelessWidget { class NewCustomAppScreen extends StatelessWidget {
final String publisherName; final String publisherName;
const NewCustomAppScreen({super.key, required this.publisherName}); final String projectId;
const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EditAppScreen(publisherName: publisherName); return EditAppScreen(publisherName: publisherName, projectId: projectId);
} }
} }

View File

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

View File

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

View File

@@ -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<List<DevProject>> 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<DevProject>()
.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)),
),
),
);
}
}

View File

@@ -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<AsyncValue<List<DevProject>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'devProjectsProvider';
}
/// See also [devProjects].
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
/// 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<List<DevProject>> 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<List<DevProject>> 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<List<DevProject>> {
/// The parameter `pubName` of this provider.
String get pubName;
}
class _DevProjectsProviderElement
extends AutoDisposeFutureProviderElement<List<DevProject>>
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

1169
swagger-develop.json Normal file

File diff suppressed because it is too large Load Diff