Compare commits

...

2 Commits

Author SHA1 Message Date
LittleSheep
4beda9200e Add developer projects 2025-08-23 02:56:28 +08:00
LittleSheep
7dfe411053 Add more developer pages (wip) 2025-08-23 02:19:07 +08:00
23 changed files with 4385 additions and 175 deletions

View File

@@ -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",

69
lib/models/bot.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/account.dart';
part 'bot.freezed.dart';
part 'bot.g.dart';
@freezed
sealed class Bot with _$Bot {
const factory Bot({
@Default('') String id,
@Default('') String name,
@Default('') String slug,
String? description,
@Default(0) int status,
SnCloudFile? picture,
SnCloudFile? background,
SnVerificationMark? verification,
BotConfig? config,
BotLinks? links,
@Default('') String publisherId,
@Default('') String appId,
DateTime? createdAt,
DateTime? updatedAt,
}) = _Bot;
factory Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json);
}
@freezed
sealed class BotConfig with _$BotConfig {
const factory BotConfig({
@Default(false) bool isPublic,
@Default(false) bool isInteractive,
@Default([]) List<String> allowedRealms,
@Default([]) List<String> allowedChatTypes,
@Default({}) Map<String, dynamic> metadata,
}) = _BotConfig;
factory BotConfig.fromJson(Map<String, dynamic> json) =>
_$BotConfigFromJson(json);
}
@freezed
sealed class BotLinks with _$BotLinks {
const factory BotLinks({
String? website,
String? documentation,
String? privacyPolicy,
String? termsOfService,
}) = _BotLinks;
factory BotLinks.fromJson(Map<String, dynamic> json) =>
_$BotLinksFromJson(json);
}
@freezed
sealed class BotSecret with _$BotSecret {
const factory BotSecret({
@Default('') String id,
@Default('') String secret,
String? description,
DateTime? expiredAt,
@Default('') String botId,
}) = _BotSecret;
factory BotSecret.fromJson(Map<String, dynamic> json) =>
_$BotSecretFromJson(json);
}

1252
lib/models/bot.freezed.dart Normal file

File diff suppressed because it is too large Load Diff

123
lib/models/bot.g.dart Normal file
View File

@@ -0,0 +1,123 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bot.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Bot _$BotFromJson(Map<String, dynamic> json) => _Bot(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
slug: json['slug'] as String? ?? '',
description: json['description'] as String?,
status: (json['status'] as num?)?.toInt() ?? 0,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
config:
json['config'] == null
? null
: BotConfig.fromJson(json['config'] as Map<String, dynamic>),
links:
json['links'] == null
? null
: BotLinks.fromJson(json['links'] as Map<String, dynamic>),
publisherId: json['publisher_id'] as String? ?? '',
appId: json['app_id'] as String? ?? '',
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
);
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'slug': instance.slug,
'description': instance.description,
'status': instance.status,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'config': instance.config?.toJson(),
'links': instance.links?.toJson(),
'publisher_id': instance.publisherId,
'app_id': instance.appId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
};
_BotConfig _$BotConfigFromJson(Map<String, dynamic> json) => _BotConfig(
isPublic: json['is_public'] as bool? ?? false,
isInteractive: json['is_interactive'] as bool? ?? false,
allowedRealms:
(json['allowed_realms'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
allowedChatTypes:
(json['allowed_chat_types'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
);
Map<String, dynamic> _$BotConfigToJson(_BotConfig instance) =>
<String, dynamic>{
'is_public': instance.isPublic,
'is_interactive': instance.isInteractive,
'allowed_realms': instance.allowedRealms,
'allowed_chat_types': instance.allowedChatTypes,
'metadata': instance.metadata,
};
_BotLinks _$BotLinksFromJson(Map<String, dynamic> json) => _BotLinks(
website: json['website'] as String?,
documentation: json['documentation'] as String?,
privacyPolicy: json['privacy_policy'] as String?,
termsOfService: json['terms_of_service'] as String?,
);
Map<String, dynamic> _$BotLinksToJson(_BotLinks instance) => <String, dynamic>{
'website': instance.website,
'documentation': instance.documentation,
'privacy_policy': instance.privacyPolicy,
'terms_of_service': instance.termsOfService,
};
_BotSecret _$BotSecretFromJson(Map<String, dynamic> json) => _BotSecret(
id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '',
description: json['description'] as String?,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
botId: json['bot_id'] as String? ?? '',
);
Map<String, dynamic> _$BotSecretToJson(_BotSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'description': instance.description,
'expired_at': instance.expiredAt?.toIso8601String(),
'bot_id': instance.botId,
};

View File

@@ -0,0 +1,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,10 +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/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';
@@ -291,30 +295,90 @@ final routerProvider = Provider<GoRouter>((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: 'developerProjectNew',
path: '/developers/:name/projects/new',
builder:
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
name: 'developerProjectEdit',
path: '/developers/:name/projects/:id/edit',
builder:
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
GoRoute(
name: 'developerProjectDetail',
path: '/developers/:name/projects/:projectId',
builder:
(context, state) => ProjectDetailScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
routes: [
GoRoute(
name: 'developerAppNew',
path: '/developers/:name/apps/new',
path: 'apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!,
),
),
GoRoute(
name: 'developerAppEdit',
path: '/developers/:name/apps/:id',
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/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,43 +15,65 @@ import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@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 resp = await client.get('/develop/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/apps',
);
return (resp.data as List)
.map((e) => CustomApp.fromJson(e))
.cast<CustomApp>()
.toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
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),
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},
pathParameters: {
'name': publisherName,
'projectId': projectId,
},
);
},
icon: const Icon(Symbols.add),
label: Text('createCustomApp').tr(),
),
],
),
body: apps.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noCustomApps').tr());
);
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(customAppsProvider(publisherName).future),
() => ref.refresh(
customAppsProvider(publisherName, projectId).future,
),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
@@ -128,6 +149,7 @@ class CustomAppsScreen extends HookConsumerWidget {
'developerAppEdit',
pathParameters: {
'name': publisherName,
'projectId': projectId,
'id': app.id,
},
);
@@ -139,10 +161,10 @@ class CustomAppsScreen extends HookConsumerWidget {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/apps/${app.id}',
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
customAppsProvider(publisherName, projectId),
);
}
});
@@ -161,7 +183,9 @@ class CustomAppsScreen extends HookConsumerWidget {
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
onRetry:
() => ref.invalidate(
customAppsProvider(publisherName, projectId),
),
),
);

View File

@@ -6,7 +6,7 @@ part of 'apps.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'c6ac78060eb51a2b208a749a81ecbe0a9c608ce1';
String _$customAppsHash() => r'c36e5ee59f16a29220dc0e9fba65e579d341a28f';
/// Copied from Dart SDK
class _SystemHash {
@@ -39,15 +39,15 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
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<ProviderOrFamily>? _dependencies = null;
@@ -68,9 +68,9 @@ class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// 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<List<CustomApp>> {
dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
CustomAppsProvider._internal(
@@ -90,9 +91,11 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
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<List<CustomApp>> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@@ -119,13 +123,16 @@ class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
@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<List<CustomApp>> {
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// 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

View File

@@ -0,0 +1,162 @@
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/bot.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bots.g.dart';
@riverpod
Future<List<Bot>> bots(Ref ref, String publisherName, String projectId) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots',
);
return (resp.data as List).map((e) => Bot.fromJson(e)).cast<Bot>().toList();
}
class BotsScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
const BotsScreen({
super.key,
required this.publisherName,
required this.projectId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final botsList = ref.watch(botsProvider(publisherName, projectId));
return botsList.when(
data: (data) {
if (data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('noBots').tr(),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
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,
},
);
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(botsProvider(publisherName, projectId)),
),
);
}
}

View File

@@ -0,0 +1,156 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bots.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botsHash() => r'a54c8b4df23f94754398706779044903fcca6eea';
/// 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 [bots].
@ProviderFor(bots)
const botsProvider = BotsFamily();
/// See also [bots].
class BotsFamily extends Family<AsyncValue<List<Bot>>> {
/// See also [bots].
const BotsFamily();
/// See also [bots].
BotsProvider call(String publisherName, String projectId) {
return BotsProvider(publisherName, projectId);
}
@override
BotsProvider getProviderOverride(covariant BotsProvider provider) {
return call(provider.publisherName, provider.projectId);
}
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'botsProvider';
}
/// See also [bots].
class BotsProvider extends AutoDisposeFutureProvider<List<Bot>> {
/// See also [bots].
BotsProvider(String publisherName, String projectId)
: this._internal(
(ref) => bots(ref as BotsRef, publisherName, projectId),
from: botsProvider,
name: r'botsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$botsHash,
dependencies: BotsFamily._dependencies,
allTransitiveDependencies: BotsFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
);
BotsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
}) : super.internal();
final String publisherName;
final String projectId;
@override
Override overrideWith(FutureOr<List<Bot>> Function(BotsRef provider) create) {
return ProviderOverride(
origin: this,
override: BotsProvider._internal(
(ref) => create(ref as BotsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
),
);
}
@override
AutoDisposeFutureProviderElement<List<Bot>> createElement() {
return _BotsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is BotsProvider &&
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);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin BotsRef on AutoDisposeFutureProviderRef<List<Bot>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `projectId` of this provider.
String get projectId;
}
class _BotsProviderElement extends AutoDisposeFutureProviderElement<List<Bot>>
with BotsRef {
_BotsProviderElement(super.provider);
@override
String get publisherName => (origin as BotsProvider).publisherName;
@override
String get projectId => (origin as BotsProvider).projectId;
}
// 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

@@ -22,21 +22,37 @@ import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@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 resp = await client.get('/develop/developers/$publisherName/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<FormState>());
@@ -283,16 +299,16 @@ class EditAppScreen extends HookConsumerWidget {
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/apps',
'/develop/developers/$publisherName/projects/$projectId/apps',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/apps/$id',
'/develop/developers/$publisherName/projects/$projectId/apps/$id',
data: data,
);
}
ref.invalidate(customAppsProvider(publisherName));
ref.invalidate(customAppsProvider(publisherName, projectId));
if (context.mounted) {
Navigator.pop(context);
}
@@ -309,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(

View File

@@ -6,7 +6,7 @@ part of 'edit_app.dart';
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'42ad937b8439c793e3c5c35568bb5fa4da017df3';
String _$customAppHash() => r'17b3d1385e59bc5ee7f13fb0f11c56cf8a9ba41f';
/// Copied from Dart SDK
class _SystemHash {
@@ -39,13 +39,13 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
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<ProviderOrFamily>? _dependencies = null;
@@ -66,9 +66,9 @@ class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// 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<CustomApp?> {
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
projectId: projectId,
id: id,
);
@@ -89,10 +90,12 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
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<CustomApp?> {
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
@@ -123,6 +127,7 @@ class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
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<CustomApp?> {
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<CustomApp?> {
/// 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;
}

View File

@@ -0,0 +1,279 @@
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';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.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';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'edit_bot.g.dart';
@riverpod
Future<Bot?> bot(
Ref ref,
String publisherName,
String projectId,
String id,
) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
);
return Bot.fromJson(resp.data);
}
class EditBotScreen extends HookConsumerWidget {
final String publisherName;
final String projectId;
final String? id;
const EditBotScreen({
super.key,
required this.publisherName,
required this.projectId,
this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final botData =
isNew ? null : ref.watch(botProvider(publisherName, projectId, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final websiteController = useTextEditingController();
final documentationController = useTextEditingController();
final isPublic = useState(false);
final isInteractive = useState(false);
useEffect(() {
if (botData?.value != null) {
nameController.text = botData!.value!.name;
slugController.text = botData.value!.slug;
descriptionController.text = botData.value!.description ?? '';
picture.value = botData.value!.picture;
websiteController.text = botData.value!.links?.website ?? '';
documentationController.text =
botData.value!.links?.documentation ?? '';
isPublic.value = botData.value!.config?.isPublic ?? false;
isInteractive.value = botData.value!.config?.isInteractive ?? false;
}
return null;
}, [botData]);
void setPicture() async {
showLoadingModal(context);
var result = await ref
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
hideLoadingModal(context);
result = await cropImage(
context,
image: result,
allowedAspectRatios: [const CropAspectRatio(height: 1, width: 1)],
);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
showLoadingModal(context);
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
picture.value = cloudFile;
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
'picture_id': picture.value?.id,
'config': {
'is_public': isPublic.value,
'is_interactive': isInteractive.value,
},
'links': {
'website':
websiteController.text.isNotEmpty ? websiteController.text : null,
'documentation':
documentationController.text.isNotEmpty
? documentationController.text
: null,
},
};
if (isNew) {
await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots',
data: data,
);
} else {
await client.patch(
'/develop/developers/$publisherName/projects/$projectId/bots/$id',
data: data,
);
}
if (context.mounted) {
context.pop();
}
}
return AppScaffold(
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
body:
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 1,
child: GestureDetector(
onTap: setPicture,
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
picture.value != null
? CloudFileWidget(
item: picture.value!,
fit: BoxFit.cover,
)
: const Icon(Symbols.smart_toy, size: 48),
),
),
).padding(bottom: 32),
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),
TextFormField(
controller: websiteController,
decoration: InputDecoration(
labelText: 'websiteUrl'.tr(),
hintText: 'https://example.com',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
TextFormField(
controller: documentationController,
decoration: InputDecoration(
labelText: 'documentationUrl'.tr(),
hintText: 'https://example.com/docs',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('isPublic').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value,
),
SwitchListTile(
title: Text('isInteractive').tr(),
value: isInteractive.value,
onChanged: (value) => isInteractive.value = value,
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,167 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_bot.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$botHash() => r'a3e412ed575c513434bc718b7920db1d017111f4';
/// 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 [bot].
@ProviderFor(bot)
const botProvider = BotFamily();
/// See also [bot].
class BotFamily extends Family<AsyncValue<Bot?>> {
/// See also [bot].
const BotFamily();
/// See also [bot].
BotProvider call(String publisherName, String projectId, String id) {
return BotProvider(publisherName, projectId, id);
}
@override
BotProvider getProviderOverride(covariant BotProvider provider) {
return call(provider.publisherName, provider.projectId, 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'botProvider';
}
/// See also [bot].
class BotProvider extends AutoDisposeFutureProvider<Bot?> {
/// See also [bot].
BotProvider(String publisherName, String projectId, String id)
: this._internal(
(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,
);
BotProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.projectId,
required this.id,
}) : super.internal();
final String publisherName;
final String projectId;
final String id;
@override
Override overrideWith(FutureOr<Bot?> Function(BotRef provider) create) {
return ProviderOverride(
origin: this,
override: BotProvider._internal(
(ref) => create(ref as BotRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
projectId: projectId,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<Bot?> createElement() {
return _BotProviderElement(this);
}
@override
bool operator ==(Object other) {
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);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
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.
String get id;
}
class _BotProviderElement extends AutoDisposeFutureProviderElement<Bot?>
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;
}
// 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

@@ -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,15 +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',
'developerProjects',
pathParameters: {
'name':
currentDeveloper.value!.publisher!.name,

View File

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

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