💄 Optimize designs in developer hub

This commit is contained in:
2025-12-06 21:39:50 +08:00
parent ac2cee10e5
commit fe386163f4
8 changed files with 450 additions and 430 deletions

View File

@@ -39,19 +39,18 @@ class AppDetailScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: onPressed: appData.value == null
appData.value == null ? null
? null : () {
: () { context.pushNamed(
context.pushNamed( 'developerAppEdit',
'developerAppEdit', pathParameters: {
pathParameters: { 'name': publisherName,
'name': publisherName, 'projectId': projectId,
'projectId': projectId, 'id': appId,
'id': appId, },
}, );
); },
},
), ),
const Gap(8), const Gap(8),
], ],
@@ -85,24 +84,34 @@ class AppDetailScreen extends HookConsumerWidget {
controller: tabController, controller: tabController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
_AppOverview(app: app), Align(
AppSecretsScreen( alignment: Alignment.topCenter,
publisherName: publisherName, child: ConstrainedBox(
projectId: projectId, constraints: const BoxConstraints(maxWidth: 640),
appId: appId, child: _AppOverview(app: app),
),
),
Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: AppSecretsScreen(
publisherName: publisherName,
projectId: projectId,
appId: appId,
),
),
), ),
], ],
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () => ref.invalidate(
onRetry: customAppProvider(publisherName, projectId, appId),
() => ref.invalidate( ),
customAppProvider(publisherName, projectId, appId), ),
),
),
), ),
); );
} }
@@ -115,6 +124,7 @@ class _AppOverview extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
AspectRatio( AspectRatio(
@@ -125,13 +135,12 @@ class _AppOverview extends StatelessWidget {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: child: app.background != null
app.background != null ? CloudFileWidget(
? CloudFileWidget( item: app.background!,
item: app.background!, fit: BoxFit.cover,
fit: BoxFit.cover, )
) : const SizedBox.shrink(),
: const SizedBox.shrink(),
), ),
Positioned( Positioned(
left: 20, left: 20,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app_secret.dart'; import 'package:island/models/custom_app_secret.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -53,37 +54,36 @@ class AppSecretsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'newSecretGenerated'.tr(),
titleText: 'newSecretGenerated'.tr(), child: Padding(
child: Padding( padding: const EdgeInsets.all(20.0),
padding: const EdgeInsets.all(20.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Text('copySecretHint'.tr()),
Text('copySecretHint'.tr()), const SizedBox(height: 16),
const SizedBox(height: 16), Container(
Container( padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(12), decoration: BoxDecoration(
decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer,
color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(8), ),
), child: SelectableText(newSecret),
child: SelectableText(newSecret),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: newSecret));
},
icon: const Icon(Symbols.copy_all),
label: Text('copy'.tr()),
),
],
), ),
), const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: newSecret));
},
icon: const Icon(Symbols.copy_all),
label: Text('copy'.tr()),
),
],
), ),
),
),
).whenComplete(() { ).whenComplete(() {
ref.invalidate( ref.invalidate(
customAppSecretsProvider(publisherName, projectId, appId), customAppSecretsProvider(publisherName, projectId, appId),
@@ -114,22 +114,38 @@ class AppSecretsScreen extends HookConsumerWidget {
controller: descriptionController, controller: descriptionController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'description'.tr(), labelText: 'description'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
),
), ),
autofocus: true, autofocus: true,
), ),
const SizedBox(height: 20), const Gap(16),
TextFormField( TextFormField(
controller: expiresInController, controller: expiresInController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'expiresIn'.tr(), labelText: 'expiresIn'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
const SizedBox(height: 20), const Gap(16),
SwitchListTile( Card(
title: Text('isOidc'.tr()), margin: EdgeInsets.zero,
value: isOidc.value, child: SwitchListTile(
onChanged: (value) => isOidc.value = value, shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
title: Text('isOidc'.tr()),
value: isOidc.value,
onChanged: (value) => isOidc.value = value,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
FilledButton.icon( FilledButton.icon(
@@ -175,14 +191,9 @@ class AppSecretsScreen extends HookConsumerWidget {
return secrets.when( return secrets.when(
data: (data) { data: (data) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: onRefresh: () => ref.refresh(
() => ref.refresh( customAppSecretsProvider(publisherName, projectId, appId).future,
customAppSecretsProvider( ),
publisherName,
projectId,
appId,
).future,
),
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(
@@ -240,14 +251,12 @@ class AppSecretsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () => ref.invalidate(
onRetry: customAppSecretsProvider(publisherName, projectId, appId),
() => ref.invalidate( ),
customAppSecretsProvider(publisherName, projectId, appId), ),
),
),
); );
} }
} }

View File

@@ -76,15 +76,14 @@ class CustomAppsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'createCustomApp'.tr(),
titleText: 'createCustomApp'.tr(), child: NewCustomAppScreen(
child: NewCustomAppScreen( publisherName: publisherName,
publisherName: publisherName, projectId: projectId,
projectId: projectId, isModal: true,
isModal: true, ),
), ),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -95,10 +94,8 @@ class CustomAppsScreen extends HookConsumerWidget {
); );
} }
return ExtendedRefreshIndicator( return ExtendedRefreshIndicator(
onRefresh: onRefresh: () =>
() => ref.refresh( ref.refresh(customAppsProvider(publisherName, projectId).future),
customAppsProvider(publisherName, projectId).future,
),
child: Column( child: Column(
children: [ children: [
const Gap(8), const Gap(8),
@@ -110,15 +107,14 @@ class CustomAppsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'createCustomApp'.tr(),
titleText: 'createCustomApp'.tr(), child: NewCustomAppScreen(
child: NewCustomAppScreen( publisherName: publisherName,
publisherName: publisherName, projectId: projectId,
projectId: projectId, isModal: true,
isModal: true, ),
), ),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -146,31 +142,20 @@ class CustomAppsScreen extends HookConsumerWidget {
}, },
child: Column( child: Column(
children: [ children: [
SizedBox( if (app.background != null)
height: 150, AspectRatio(
child: Stack( aspectRatio: 16 / 7,
fit: StackFit.expand, child: CloudFileWidget(
children: [ item: app.background!,
if (app.background != null) fit: BoxFit.cover,
CloudFileWidget( ).clipRRect(topLeft: 8, topRight: 8),
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( ListTile(
title: Text(app.name), title: Text(app.name),
leading: ProfilePictureWidget(
fileId: app.picture?.id,
fallbackIcon: Symbols.apps,
),
subtitle: Text( subtitle: Text(
app.slug, app.slug,
style: GoogleFonts.robotoMono(fontSize: 12), style: GoogleFonts.robotoMono(fontSize: 12),
@@ -180,52 +165,48 @@ class CustomAppsScreen extends HookConsumerWidget {
right: 12, right: 12,
), ),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: itemBuilder: (context) => [
(context) => [ PopupMenuItem(
PopupMenuItem( value: 'edit',
value: 'edit', child: Row(
child: Row( children: [
children: [ const Icon(Symbols.edit),
const Icon(Symbols.edit), const SizedBox(width: 12),
const SizedBox(width: 12), Text('edit').tr(),
Text('edit').tr(), ],
], ),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
), ),
), const SizedBox(width: 12),
PopupMenuItem( Text(
value: 'delete', 'delete',
child: Row( style: TextStyle(color: Colors.red),
children: [ ).tr(),
const Icon( ],
Symbols.delete, ),
color: Colors.red, ),
), ],
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(
color: Colors.red,
),
).tr(),
],
),
),
],
onSelected: (value) { onSelected: (value) {
if (value == 'edit') { if (value == 'edit') {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'editCustomApp'.tr(),
titleText: 'editCustomApp'.tr(), child: EditAppScreen(
child: EditAppScreen( publisherName: publisherName,
publisherName: publisherName, projectId: projectId,
projectId: projectId, id: app.id,
id: app.id, isModal: true,
isModal: true, ),
), ),
),
); );
} else if (value == 'delete') { } else if (value == 'delete') {
showConfirmAlert( showConfirmAlert(
@@ -264,14 +245,11 @@ class CustomAppsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () =>
onRetry: ref.invalidate(customAppsProvider(publisherName, projectId)),
() => ref.invalidate( ),
customAppsProvider(publisherName, projectId),
),
),
); );
} }
} }

View File

@@ -36,19 +36,18 @@ class BotDetailScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: onPressed: botData.value == null
botData.value == null ? null
? null : () {
: () { context.pushNamed(
context.pushNamed( 'developerBotEdit',
'developerBotEdit', pathParameters: {
pathParameters: { 'name': publisherName,
'name': publisherName, 'projectId': projectId,
'projectId': projectId, 'id': botId,
'id': botId, },
}, );
); },
},
), ),
], ],
bottom: TabBar( bottom: TabBar(
@@ -84,24 +83,33 @@ class BotDetailScreen extends HookConsumerWidget {
controller: tabController, controller: tabController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
_BotOverview(bot: bot), Align(
BotKeysScreen( alignment: Alignment.topCenter,
publisherName: publisherName, child: ConstrainedBox(
projectId: projectId, constraints: const BoxConstraints(maxWidth: 640),
botId: botId, child: _BotOverview(bot: bot),
),
),
Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: BotKeysScreen(
publisherName: publisherName,
projectId: projectId,
botId: botId,
),
),
), ),
], ],
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () =>
onRetry: ref.invalidate(botProvider(publisherName, projectId, botId)),
() => ref.invalidate( ),
botProvider(publisherName, projectId, botId),
),
),
), ),
); );
} }
@@ -124,13 +132,12 @@ class _BotOverview extends StatelessWidget {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: child: bot.account.profile.background != null
bot.account.profile.background != null ? CloudFileWidget(
? CloudFileWidget( item: bot.account.profile.background!,
item: bot.account.profile.background!, fit: BoxFit.cover,
fit: BoxFit.cover, )
) : const SizedBox.shrink(),
: const SizedBox.shrink(),
), ),
Positioned( Positioned(
left: 20, left: 20,

View File

@@ -53,37 +53,36 @@ class BotKeysScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'newKeyGenerated'.tr(),
titleText: 'newKeyGenerated'.tr(), child: Padding(
child: Padding( padding: const EdgeInsets.all(20.0),
padding: const EdgeInsets.all(20.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Text('copyKeyHint'.tr()),
Text('copyKeyHint'.tr()), const SizedBox(height: 16),
const SizedBox(height: 16), Container(
Container( padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(12), decoration: BoxDecoration(
decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer,
color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(8), ),
), child: SelectableText(token),
child: SelectableText(token),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: token));
},
icon: const Icon(Symbols.copy_all),
label: Text('copy'.tr()),
),
],
), ),
), const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: token));
},
icon: const Icon(Symbols.copy_all),
label: Text('copy'.tr()),
),
],
), ),
),
),
).whenComplete(() { ).whenComplete(() {
ref.invalidate(botKeysProvider(publisherName, projectId, botId)); ref.invalidate(botKeysProvider(publisherName, projectId, botId));
}); });
@@ -94,45 +93,50 @@ class BotKeysScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( heightFactor: 0.7,
titleText: 'newBotKey'.tr(), titleText: 'newBotKey'.tr(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextFormField( TextFormField(
controller: keyNameController, controller: keyNameController,
decoration: InputDecoration(labelText: 'keyName'.tr()), decoration: InputDecoration(
autofocus: true, labelText: 'keyName'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
const SizedBox(height: 20), ),
FilledButton.icon( autofocus: true,
onPressed: () async {
if (keyNameController.text.isEmpty) return;
final keyName = keyNameController.text;
Navigator.pop(context); // Close the sheet
try {
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
data: {'label': keyName},
);
final newApiKey = SnAccountApiKey.fromJson(resp.data);
showNewKeySheet(newApiKey);
} catch (e) {
showErrorAlert(e.toString());
}
},
icon: const Icon(Symbols.add),
label: Text('create'.tr()),
),
],
), ),
), const SizedBox(height: 20),
FilledButton.icon(
onPressed: () async {
if (keyNameController.text.isEmpty) return;
final keyName = keyNameController.text;
Navigator.pop(context); // Close the sheet
try {
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
data: {'label': keyName},
);
final newApiKey = SnAccountApiKey.fromJson(resp.data);
showNewKeySheet(newApiKey);
} catch (e) {
showErrorAlert(e.toString());
}
},
icon: const Icon(Symbols.add),
label: Text('create'.tr()),
),
],
), ),
),
),
); );
} }
@@ -189,92 +193,79 @@ class BotKeysScreen extends HookConsumerWidget {
ListTile( ListTile(
leading: const Icon(Symbols.add), leading: const Icon(Symbols.add),
title: Text('newBotKey'.tr()), title: Text('newBotKey'.tr()),
trailing: const Icon(Symbols.chevron_right),
onTap: createKey, onTap: createKey,
), ),
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: child: data.isEmpty
data.isEmpty ? Center(child: Text('noBotKeys'.tr()))
? Center(child: Text('noBotKeys'.tr())) : RefreshIndicator(
: RefreshIndicator( onRefresh: () => ref.refresh(
onRefresh: botKeysProvider(publisherName, projectId, botId).future,
() => ref.refresh( ),
botKeysProvider( child: ListView.builder(
publisherName, padding: EdgeInsets.zero,
projectId, itemCount: data.length,
botId, itemBuilder: (context, index) {
).future, final apiKey = data[index];
return ListTile(
title: Text(apiKey.label),
subtitle: Text(apiKey.createdAt.formatSystem()),
contentPadding: EdgeInsets.only(
left: 16,
right: 12,
), ),
child: ListView.builder( trailing: PopupMenuButton(
padding: EdgeInsets.zero, itemBuilder: (context) => [
itemCount: data.length, PopupMenuItem(
itemBuilder: (context, index) { value: 'rotate',
final apiKey = data[index]; child: Row(
return ListTile( children: [
title: Text(apiKey.label), const Icon(Symbols.refresh),
subtitle: Text(apiKey.createdAt.formatSystem()), const Gap(12),
contentPadding: EdgeInsets.only( Text('rotateKey'.tr()),
left: 16, ],
right: 12, ),
), ),
trailing: PopupMenuButton( PopupMenuItem(
itemBuilder: value: 'revoke',
(context) => [ child: Row(
PopupMenuItem( children: [
value: 'rotate', const Icon(
child: Row( Symbols.delete,
children: [ color: Colors.red,
const Icon(Symbols.refresh),
const Gap(12),
Text('rotateKey'.tr()),
],
),
), ),
PopupMenuItem( const Gap(12),
value: 'revoke', Text(
child: Row( 'revoke'.tr(),
children: [ style: TextStyle(color: Colors.red),
const Icon(
Symbols.delete,
color: Colors.red,
),
const Gap(12),
Text(
'revoke'.tr(),
style: TextStyle(
color: Colors.red,
),
),
],
),
), ),
], ],
onSelected: (value) { ),
if (value == 'rotate') { ),
rotateKey(apiKey.id); ],
} else if (value == 'revoke') { onSelected: (value) {
revokeKey(apiKey.id); if (value == 'rotate') {
} rotateKey(apiKey.id);
}, } else if (value == 'revoke') {
), revokeKey(apiKey.id);
); }
}, },
), ),
);
},
), ),
),
), ),
], ],
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () =>
onRetry: ref.invalidate(botKeysProvider(publisherName, projectId, botId)),
() => ref.invalidate( ),
botKeysProvider(publisherName, projectId, botId),
),
),
); );
} }
} }

View File

@@ -54,15 +54,14 @@ class BotsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'createBot'.tr(),
titleText: 'createBot'.tr(), child: NewBotScreen(
child: NewBotScreen( publisherName: publisherName,
publisherName: publisherName, projectId: projectId,
projectId: projectId, isModal: true,
isModal: true, ),
), ),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -73,8 +72,8 @@ class BotsScreen extends HookConsumerWidget {
); );
} }
return ExtendedRefreshIndicator( return ExtendedRefreshIndicator(
onRefresh: onRefresh: () =>
() => ref.refresh(botsProvider(publisherName, projectId).future), ref.refresh(botsProvider(publisherName, projectId).future),
child: Column( child: Column(
children: [ children: [
const Gap(8), const Gap(8),
@@ -86,15 +85,14 @@ class BotsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( titleText: 'createBot'.tr(),
titleText: 'createBot'.tr(), child: NewBotScreen(
child: NewBotScreen( publisherName: publisherName,
publisherName: publisherName, projectId: projectId,
projectId: projectId, isModal: true,
isModal: true, ),
), ),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -108,23 +106,30 @@ class BotsScreen extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final bot = data[index]; final bot = data[index];
return Card( return Card(
child: ListTile( child: Column(
shape: const RoundedRectangleBorder( children: [
borderRadius: BorderRadius.all(Radius.circular(8.0)), if (bot.account.profile.background != null)
), AspectRatio(
leading: CircleAvatar( aspectRatio: 16 / 7,
child: child: CloudFileWidget(
bot.account.profile.picture != null item: bot.account.profile.background!,
? ProfilePictureWidget( fit: BoxFit.cover,
file: bot.account.profile.picture!, ).clipRRect(topLeft: 8, topRight: 8),
) ),
: const Icon(Symbols.smart_toy), ListTile(
), shape: const RoundedRectangleBorder(
title: Text(bot.account.nick), borderRadius: BorderRadius.all(
subtitle: Text(bot.account.name), Radius.circular(8.0),
trailing: PopupMenuButton( ),
itemBuilder: ),
(context) => [ leading: ProfilePictureWidget(
fallbackIcon: Symbols.smart_toy,
file: bot.account.profile.picture,
),
title: Text(bot.account.nick),
subtitle: Text(bot.account.name),
trailing: PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: 'edit', value: 'edit',
child: Row( child: Row(
@@ -152,13 +157,12 @@ class BotsScreen extends HookConsumerWidget {
), ),
), ),
], ],
onSelected: (value) { onSelected: (value) {
if (value == 'edit') { if (value == 'edit') {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'editBot'.tr(), titleText: 'editBot'.tr(),
child: EditBotScreen( child: EditBotScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -167,36 +171,40 @@ class BotsScreen extends HookConsumerWidget {
isModal: true, isModal: true,
), ),
), ),
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteBotHint'.tr(),
'deleteBot'.tr(),
isDanger: true,
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
);
ref.invalidate(
botsProvider(publisherName, projectId),
); );
} else if (value == 'delete') {
showConfirmAlert(
'deleteBotHint'.tr(),
'deleteBot'.tr(),
isDanger: true,
).then((confirm) {
if (confirm) {
final client = ref.read(
apiClientProvider,
);
client.delete(
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
);
ref.invalidate(
botsProvider(publisherName, projectId),
);
}
});
} }
}); },
} ),
}, onTap: () {
), context.pushNamed(
onTap: () { 'developerBotDetail',
context.pushNamed( pathParameters: {
'developerBotDetail', 'name': publisherName,
pathParameters: { 'projectId': projectId,
'name': publisherName, 'botId': bot.id,
'projectId': projectId, },
'botId': bot.id, );
}, },
); ),
}, ],
), ),
); );
}, },
@@ -207,12 +215,10 @@ class BotsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget( error: err,
error: err, onRetry: () => ref.invalidate(botsProvider(publisherName, projectId)),
onRetry: ),
() => ref.invalidate(botsProvider(publisherName, projectId)),
),
); );
} }
} }

View File

@@ -24,6 +24,16 @@ class ProjectDetailView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2); final tabController = useTabController(initialLength: 2);
final currentDest = useState(0);
useEffect(() {
tabController.addListener(() {
if (tabController.indexIsChanging) {
currentDest.value = tabController.index;
}
});
return null;
});
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
@@ -38,14 +48,13 @@ class ProjectDetailView extends HookConsumerWidget {
child: NavigationRail( child: NavigationRail(
extended: isWiderScreen(context), extended: isWiderScreen(context),
scrollable: true, scrollable: true,
labelType: labelType: isWiderScreen(context)
isWiderScreen(context) ? null
? null : NavigationRailLabelType.selected,
: NavigationRailLabelType.selected,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
selectedIndex: tabController.index, selectedIndex: currentDest.value,
onDestinationSelected: onDestinationSelected: (index) =>
(index) => tabController.animateTo(index), tabController.animateTo(index),
destinations: [ destinations: [
NavigationRailDestination( NavigationRailDestination(
icon: Icon(Icons.apps), icon: Icon(Icons.apps),

View File

@@ -1,4 +1,7 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -8,6 +11,7 @@ import 'package:island/models/post.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/post_detail.dart'; import 'package:island/screens/posts/post_detail.dart';
import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/compose_storage_db.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_card.dart'; import 'package:island/widgets/post/compose_card.dart';
import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/compose_shared.dart';
@@ -171,7 +175,14 @@ class PostComposeSheet extends HookConsumerWidget {
), ),
]; ];
// Tablet will show a virtual keyboard, so we adjust the height factor accordingly
final isTablet =
isWideScreen(context) &&
!kIsWeb &&
(Platform.isAndroid || Platform.isAndroid);
return SheetScaffold( return SheetScaffold(
heightFactor: isTablet ? 0.95 : 0.8,
titleText: 'postCompose'.tr(), titleText: 'postCompose'.tr(),
actions: actions, actions: actions,
child: PostComposeCard( child: PostComposeCard(