♻️ Refactor the app management to use sheet
This commit is contained in:
		@@ -6,8 +6,11 @@ import 'package:google_fonts/google_fonts.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					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/screens/developers/edit_app.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/developers/new_app.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
 | 
					import 'package:island/widgets/extended_refresh_indicator.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';
 | 
				
			||||||
@@ -70,12 +73,18 @@ class CustomAppsScreen extends HookConsumerWidget {
 | 
				
			|||||||
                const SizedBox(height: 16),
 | 
					                const SizedBox(height: 16),
 | 
				
			||||||
                ElevatedButton.icon(
 | 
					                ElevatedButton.icon(
 | 
				
			||||||
                  onPressed: () {
 | 
					                  onPressed: () {
 | 
				
			||||||
                    context.pushNamed(
 | 
					                    showModalBottomSheet(
 | 
				
			||||||
                      'developerAppNew',
 | 
					                      context: context,
 | 
				
			||||||
                      pathParameters: {
 | 
					                      isScrollControlled: true,
 | 
				
			||||||
                        'name': publisherName,
 | 
					                      builder:
 | 
				
			||||||
                        'projectId': projectId,
 | 
					                          (context) => SheetScaffold(
 | 
				
			||||||
                      },
 | 
					                            titleText: 'createCustomApp'.tr(),
 | 
				
			||||||
 | 
					                            child: NewCustomAppScreen(
 | 
				
			||||||
 | 
					                              publisherName: publisherName,
 | 
				
			||||||
 | 
					                              projectId: projectId,
 | 
				
			||||||
 | 
					                              isModal: true,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  icon: const Icon(Symbols.add),
 | 
					                  icon: const Icon(Symbols.add),
 | 
				
			||||||
@@ -98,12 +107,18 @@ class CustomAppsScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  title: Text('customApps').tr().padding(horizontal: 8),
 | 
					                  title: Text('customApps').tr().padding(horizontal: 8),
 | 
				
			||||||
                  trailing: IconButton(
 | 
					                  trailing: IconButton(
 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      context.pushNamed(
 | 
					                      showModalBottomSheet(
 | 
				
			||||||
                        'developerAppNew',
 | 
					                        context: context,
 | 
				
			||||||
                        pathParameters: {
 | 
					                        isScrollControlled: true,
 | 
				
			||||||
                          'name': publisherName,
 | 
					                        builder:
 | 
				
			||||||
                          'projectId': projectId,
 | 
					                            (context) => SheetScaffold(
 | 
				
			||||||
                        },
 | 
					                              titleText: 'createCustomApp'.tr(),
 | 
				
			||||||
 | 
					                              child: NewCustomAppScreen(
 | 
				
			||||||
 | 
					                                publisherName: publisherName,
 | 
				
			||||||
 | 
					                                projectId: projectId,
 | 
				
			||||||
 | 
					                                isModal: true,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    icon: const Icon(Symbols.add),
 | 
					                    icon: const Icon(Symbols.add),
 | 
				
			||||||
@@ -198,13 +213,19 @@ class CustomAppsScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                    ],
 | 
					                                    ],
 | 
				
			||||||
                                onSelected: (value) {
 | 
					                                onSelected: (value) {
 | 
				
			||||||
                                  if (value == 'edit') {
 | 
					                                  if (value == 'edit') {
 | 
				
			||||||
                                    context.pushNamed(
 | 
					                                    showModalBottomSheet(
 | 
				
			||||||
                                      'developerAppEdit',
 | 
					                                      context: context,
 | 
				
			||||||
                                      pathParameters: {
 | 
					                                      isScrollControlled: true,
 | 
				
			||||||
                                        'name': publisherName,
 | 
					                                      builder:
 | 
				
			||||||
                                        'projectId': projectId,
 | 
					                                          (context) => SheetScaffold(
 | 
				
			||||||
                                        'id': app.id,
 | 
					                                            titleText: 'editCustomApp'.tr(),
 | 
				
			||||||
                                      },
 | 
					                                            child: EditAppScreen(
 | 
				
			||||||
 | 
					                                              publisherName: publisherName,
 | 
				
			||||||
 | 
					                                              projectId: projectId,
 | 
				
			||||||
 | 
					                                              id: app.id,
 | 
				
			||||||
 | 
					                                              isModal: true,
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
                                    );
 | 
					                                    );
 | 
				
			||||||
                                  } else if (value == 'delete') {
 | 
					                                  } else if (value == 'delete') {
 | 
				
			||||||
                                    showConfirmAlert(
 | 
					                                    showConfirmAlert(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,11 +39,13 @@ class EditAppScreen extends HookConsumerWidget {
 | 
				
			|||||||
  final String publisherName;
 | 
					  final String publisherName;
 | 
				
			||||||
  final String projectId;
 | 
					  final String projectId;
 | 
				
			||||||
  final String? id;
 | 
					  final String? id;
 | 
				
			||||||
 | 
					  final bool isModal;
 | 
				
			||||||
  const EditAppScreen({
 | 
					  const EditAppScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.publisherName,
 | 
					    required this.publisherName,
 | 
				
			||||||
    required this.projectId,
 | 
					    required this.projectId,
 | 
				
			||||||
    this.id,
 | 
					    this.id,
 | 
				
			||||||
 | 
					    this.isModal = false,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -177,7 +179,12 @@ class EditAppScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    TextFormField(
 | 
					                    TextFormField(
 | 
				
			||||||
                      controller: scopeController,
 | 
					                      controller: scopeController,
 | 
				
			||||||
                      decoration: InputDecoration(labelText: 'scopeName'.tr()),
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
 | 
					                        labelText: 'scopeName'.tr(),
 | 
				
			||||||
 | 
					                        border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                          borderRadius: BorderRadius.all(Radius.circular(12)),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    const SizedBox(height: 20),
 | 
					                    const SizedBox(height: 20),
 | 
				
			||||||
                    FilledButton.tonalIcon(
 | 
					                    FilledButton.tonalIcon(
 | 
				
			||||||
@@ -220,6 +227,9 @@ class EditAppScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        hintText: 'https://example.com/auth/callback',
 | 
					                        hintText: 'https://example.com/auth/callback',
 | 
				
			||||||
                        helperText: 'redirectUriHint'.tr(),
 | 
					                        helperText: 'redirectUriHint'.tr(),
 | 
				
			||||||
                        helperMaxLines: 3,
 | 
					                        helperMaxLines: 3,
 | 
				
			||||||
 | 
					                        border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                          borderRadius: BorderRadius.all(Radius.circular(12)),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      keyboardType: TextInputType.url,
 | 
					                      keyboardType: TextInputType.url,
 | 
				
			||||||
                      validator: (value) {
 | 
					                      validator: (value) {
 | 
				
			||||||
@@ -316,270 +326,298 @@ class EditAppScreen extends HookConsumerWidget {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final bodyContent =
 | 
				
			||||||
 | 
					        app == null && !isNew
 | 
				
			||||||
 | 
					            ? const Center(child: CircularProgressIndicator())
 | 
				
			||||||
 | 
					            : app?.hasError == true && !isNew
 | 
				
			||||||
 | 
					            ? ResponseErrorWidget(
 | 
				
			||||||
 | 
					              error: app!.error,
 | 
				
			||||||
 | 
					              onRetry:
 | 
				
			||||||
 | 
					                  () => ref.invalidate(
 | 
				
			||||||
 | 
					                    customAppProvider(publisherName, projectId, id!),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            : SingleChildScrollView(
 | 
				
			||||||
 | 
					              child: Column(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  AspectRatio(
 | 
				
			||||||
 | 
					                    aspectRatio: 16 / 7,
 | 
				
			||||||
 | 
					                    child: Stack(
 | 
				
			||||||
 | 
					                      clipBehavior: Clip.none,
 | 
				
			||||||
 | 
					                      fit: StackFit.expand,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        GestureDetector(
 | 
				
			||||||
 | 
					                          child: Container(
 | 
				
			||||||
 | 
					                            color:
 | 
				
			||||||
 | 
					                                Theme.of(
 | 
				
			||||||
 | 
					                                  context,
 | 
				
			||||||
 | 
					                                ).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					                            child:
 | 
				
			||||||
 | 
					                                background.value != null
 | 
				
			||||||
 | 
					                                    ? CloudFileWidget(
 | 
				
			||||||
 | 
					                                      item: background.value!,
 | 
				
			||||||
 | 
					                                      fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    : const SizedBox.shrink(),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          onTap: () {
 | 
				
			||||||
 | 
					                            setPicture('background');
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        Positioned(
 | 
				
			||||||
 | 
					                          left: 20,
 | 
				
			||||||
 | 
					                          bottom: -32,
 | 
				
			||||||
 | 
					                          child: GestureDetector(
 | 
				
			||||||
 | 
					                            child: ProfilePictureWidget(
 | 
				
			||||||
 | 
					                              fileId: picture.value?.id,
 | 
				
			||||||
 | 
					                              radius: 40,
 | 
				
			||||||
 | 
					                              fallbackIcon: Symbols.apps,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onTap: () {
 | 
				
			||||||
 | 
					                              setPicture('picture');
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ).padding(bottom: 32),
 | 
				
			||||||
 | 
					                  Form(
 | 
				
			||||||
 | 
					                    key: formKey,
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        TextFormField(
 | 
				
			||||||
 | 
					                          controller: nameController,
 | 
				
			||||||
 | 
					                          decoration: InputDecoration(
 | 
				
			||||||
 | 
					                            labelText: 'name'.tr(),
 | 
				
			||||||
 | 
					                            border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                              borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                Radius.circular(12),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          onTapOutside:
 | 
				
			||||||
 | 
					                              (_) =>
 | 
				
			||||||
 | 
					                                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                        TextFormField(
 | 
				
			||||||
 | 
					                          controller: slugController,
 | 
				
			||||||
 | 
					                          decoration: InputDecoration(
 | 
				
			||||||
 | 
					                            labelText: 'slug'.tr(),
 | 
				
			||||||
 | 
					                            helperText: 'slugHint'.tr(),
 | 
				
			||||||
 | 
					                            border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                              borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                Radius.circular(12),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          onTapOutside:
 | 
				
			||||||
 | 
					                              (_) =>
 | 
				
			||||||
 | 
					                                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                        TextFormField(
 | 
				
			||||||
 | 
					                          controller: descriptionController,
 | 
				
			||||||
 | 
					                          decoration: InputDecoration(
 | 
				
			||||||
 | 
					                            labelText: 'description'.tr(),
 | 
				
			||||||
 | 
					                            alignLabelWithHint: true,
 | 
				
			||||||
 | 
					                            border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                              borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                Radius.circular(12),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          maxLines: 3,
 | 
				
			||||||
 | 
					                          onTapOutside:
 | 
				
			||||||
 | 
					                              (_) =>
 | 
				
			||||||
 | 
					                                  FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                        ExpansionPanelList(
 | 
				
			||||||
 | 
					                          expansionCallback: (index, isExpanded) {
 | 
				
			||||||
 | 
					                            switch (index) {
 | 
				
			||||||
 | 
					                              case 0:
 | 
				
			||||||
 | 
					                                enableLinks.value = isExpanded;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                              case 1:
 | 
				
			||||||
 | 
					                                oauthEnabled.value = isExpanded;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            ExpansionPanel(
 | 
				
			||||||
 | 
					                              headerBuilder:
 | 
				
			||||||
 | 
					                                  (context, isExpanded) =>
 | 
				
			||||||
 | 
					                                      ListTile(title: Text('appLinks').tr()),
 | 
				
			||||||
 | 
					                              body: Column(
 | 
				
			||||||
 | 
					                                spacing: 16,
 | 
				
			||||||
 | 
					                                children: [
 | 
				
			||||||
 | 
					                                  TextFormField(
 | 
				
			||||||
 | 
					                                    controller: homePageController,
 | 
				
			||||||
 | 
					                                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                      labelText: 'homePageUrl'.tr(),
 | 
				
			||||||
 | 
					                                      hintText: 'https://example.com',
 | 
				
			||||||
 | 
					                                      border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                                        borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                          Radius.circular(12),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    keyboardType: TextInputType.url,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  TextFormField(
 | 
				
			||||||
 | 
					                                    controller: privacyPolicyController,
 | 
				
			||||||
 | 
					                                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                      labelText: 'privacyPolicyUrl'.tr(),
 | 
				
			||||||
 | 
					                                      hintText: 'https://example.com/privacy',
 | 
				
			||||||
 | 
					                                      border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                                        borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                          Radius.circular(12),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    keyboardType: TextInputType.url,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  TextFormField(
 | 
				
			||||||
 | 
					                                    controller: termsController,
 | 
				
			||||||
 | 
					                                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                                      labelText: 'termsOfServiceUrl'.tr(),
 | 
				
			||||||
 | 
					                                      hintText: 'https://example.com/terms',
 | 
				
			||||||
 | 
					                                      border: OutlineInputBorder(
 | 
				
			||||||
 | 
					                                        borderRadius: BorderRadius.all(
 | 
				
			||||||
 | 
					                                          Radius.circular(12),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    keyboardType: TextInputType.url,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ],
 | 
				
			||||||
 | 
					                              ).padding(horizontal: 16, bottom: 24),
 | 
				
			||||||
 | 
					                              isExpanded: enableLinks.value,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            ExpansionPanel(
 | 
				
			||||||
 | 
					                              headerBuilder:
 | 
				
			||||||
 | 
					                                  (context, isExpanded) =>
 | 
				
			||||||
 | 
					                                      ListTile(title: Text('oauthConfig').tr()),
 | 
				
			||||||
 | 
					                              body: Column(
 | 
				
			||||||
 | 
					                                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                                children: [
 | 
				
			||||||
 | 
					                                  Text('redirectUris'.tr()),
 | 
				
			||||||
 | 
					                                  Card(
 | 
				
			||||||
 | 
					                                    margin: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                                      vertical: 8,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    child: Column(
 | 
				
			||||||
 | 
					                                      children: [
 | 
				
			||||||
 | 
					                                        ...redirectUris.value.map(
 | 
				
			||||||
 | 
					                                          (uri) => ListTile(
 | 
				
			||||||
 | 
					                                            title: Text(uri),
 | 
				
			||||||
 | 
					                                            trailing: IconButton(
 | 
				
			||||||
 | 
					                                              icon: const Icon(Symbols.delete),
 | 
				
			||||||
 | 
					                                              onPressed: () {
 | 
				
			||||||
 | 
					                                                redirectUris.value =
 | 
				
			||||||
 | 
					                                                    redirectUris.value
 | 
				
			||||||
 | 
					                                                        .where((u) => u != uri)
 | 
				
			||||||
 | 
					                                                        .toList();
 | 
				
			||||||
 | 
					                                              },
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                        if (redirectUris.value.isNotEmpty)
 | 
				
			||||||
 | 
					                                          const Divider(height: 1),
 | 
				
			||||||
 | 
					                                        ListTile(
 | 
				
			||||||
 | 
					                                          leading: const Icon(Symbols.add),
 | 
				
			||||||
 | 
					                                          title: Text('addRedirectUri'.tr()),
 | 
				
			||||||
 | 
					                                          onTap: showAddRedirectUriDialog,
 | 
				
			||||||
 | 
					                                          shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					                                            borderRadius: BorderRadius.circular(
 | 
				
			||||||
 | 
					                                              8,
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ],
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                                  Text('allowedScopes'.tr()),
 | 
				
			||||||
 | 
					                                  Card(
 | 
				
			||||||
 | 
					                                    margin: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                                      vertical: 8,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    child: Column(
 | 
				
			||||||
 | 
					                                      children: [
 | 
				
			||||||
 | 
					                                        ...allowedScopes.value.map(
 | 
				
			||||||
 | 
					                                          (scope) => ListTile(
 | 
				
			||||||
 | 
					                                            title: Text(scope),
 | 
				
			||||||
 | 
					                                            trailing: IconButton(
 | 
				
			||||||
 | 
					                                              icon: const Icon(Symbols.delete),
 | 
				
			||||||
 | 
					                                              onPressed: () {
 | 
				
			||||||
 | 
					                                                allowedScopes.value =
 | 
				
			||||||
 | 
					                                                    allowedScopes.value
 | 
				
			||||||
 | 
					                                                        .where(
 | 
				
			||||||
 | 
					                                                          (s) => s != scope,
 | 
				
			||||||
 | 
					                                                        )
 | 
				
			||||||
 | 
					                                                        .toList();
 | 
				
			||||||
 | 
					                                              },
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                        if (allowedScopes.value.isNotEmpty)
 | 
				
			||||||
 | 
					                                          const Divider(height: 1),
 | 
				
			||||||
 | 
					                                        ListTile(
 | 
				
			||||||
 | 
					                                          leading: const Icon(Symbols.add),
 | 
				
			||||||
 | 
					                                          title: Text('add').tr(),
 | 
				
			||||||
 | 
					                                          onTap: showAddScopeDialog,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ],
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                                  SwitchListTile(
 | 
				
			||||||
 | 
					                                    title: Text('requirePkce'.tr()),
 | 
				
			||||||
 | 
					                                    value: requirePkce.value,
 | 
				
			||||||
 | 
					                                    onChanged:
 | 
				
			||||||
 | 
					                                        (value) => requirePkce.value = value,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  SwitchListTile(
 | 
				
			||||||
 | 
					                                    title: Text('allowOfflineAccess'.tr()),
 | 
				
			||||||
 | 
					                                    value: allowOfflineAccess.value,
 | 
				
			||||||
 | 
					                                    onChanged:
 | 
				
			||||||
 | 
					                                        (value) =>
 | 
				
			||||||
 | 
					                                            allowOfflineAccess.value = value,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ],
 | 
				
			||||||
 | 
					                              ).padding(horizontal: 16, bottom: 24),
 | 
				
			||||||
 | 
					                              isExpanded: oauthEnabled.value,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                        Align(
 | 
				
			||||||
 | 
					                          alignment: Alignment.centerRight,
 | 
				
			||||||
 | 
					                          child: TextButton.icon(
 | 
				
			||||||
 | 
					                            onPressed: submitting.value ? null : performAction,
 | 
				
			||||||
 | 
					                            label: Text('saveChanges'.tr()),
 | 
				
			||||||
 | 
					                            icon: const Icon(Symbols.save),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).padding(all: 24),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isModal) {
 | 
				
			||||||
 | 
					      return bodyContent;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      isNoBackground: false,
 | 
					      isNoBackground: false,
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
 | 
					        title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body:
 | 
					      body: bodyContent,
 | 
				
			||||||
          app == null && !isNew
 | 
					 | 
				
			||||||
              ? const Center(child: CircularProgressIndicator())
 | 
					 | 
				
			||||||
              : app?.hasError == true && !isNew
 | 
					 | 
				
			||||||
              ? ResponseErrorWidget(
 | 
					 | 
				
			||||||
                error: app!.error,
 | 
					 | 
				
			||||||
                onRetry:
 | 
					 | 
				
			||||||
                    () => ref.invalidate(
 | 
					 | 
				
			||||||
                      customAppProvider(publisherName, projectId, id!),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
              : SingleChildScrollView(
 | 
					 | 
				
			||||||
                child: Column(
 | 
					 | 
				
			||||||
                  children: [
 | 
					 | 
				
			||||||
                    AspectRatio(
 | 
					 | 
				
			||||||
                      aspectRatio: 16 / 7,
 | 
					 | 
				
			||||||
                      child: Stack(
 | 
					 | 
				
			||||||
                        clipBehavior: Clip.none,
 | 
					 | 
				
			||||||
                        fit: StackFit.expand,
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          GestureDetector(
 | 
					 | 
				
			||||||
                            child: Container(
 | 
					 | 
				
			||||||
                              color:
 | 
					 | 
				
			||||||
                                  Theme.of(
 | 
					 | 
				
			||||||
                                    context,
 | 
					 | 
				
			||||||
                                  ).colorScheme.surfaceContainerHigh,
 | 
					 | 
				
			||||||
                              child:
 | 
					 | 
				
			||||||
                                  background.value != null
 | 
					 | 
				
			||||||
                                      ? CloudFileWidget(
 | 
					 | 
				
			||||||
                                        item: background.value!,
 | 
					 | 
				
			||||||
                                        fit: BoxFit.cover,
 | 
					 | 
				
			||||||
                                      )
 | 
					 | 
				
			||||||
                                      : const SizedBox.shrink(),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                            onTap: () {
 | 
					 | 
				
			||||||
                              setPicture('background');
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          Positioned(
 | 
					 | 
				
			||||||
                            left: 20,
 | 
					 | 
				
			||||||
                            bottom: -32,
 | 
					 | 
				
			||||||
                            child: GestureDetector(
 | 
					 | 
				
			||||||
                              child: ProfilePictureWidget(
 | 
					 | 
				
			||||||
                                fileId: picture.value?.id,
 | 
					 | 
				
			||||||
                                radius: 40,
 | 
					 | 
				
			||||||
                                fallbackIcon: Symbols.apps,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                              onTap: () {
 | 
					 | 
				
			||||||
                                setPicture('picture');
 | 
					 | 
				
			||||||
                              },
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ).padding(bottom: 32),
 | 
					 | 
				
			||||||
                    Form(
 | 
					 | 
				
			||||||
                      key: formKey,
 | 
					 | 
				
			||||||
                      child: Column(
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          TextFormField(
 | 
					 | 
				
			||||||
                            controller: nameController,
 | 
					 | 
				
			||||||
                            decoration: InputDecoration(labelText: 'name'.tr()),
 | 
					 | 
				
			||||||
                            onTapOutside:
 | 
					 | 
				
			||||||
                                (_) =>
 | 
					 | 
				
			||||||
                                    FocusManager.instance.primaryFocus
 | 
					 | 
				
			||||||
                                        ?.unfocus(),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                          TextFormField(
 | 
					 | 
				
			||||||
                            controller: slugController,
 | 
					 | 
				
			||||||
                            decoration: InputDecoration(
 | 
					 | 
				
			||||||
                              labelText: 'slug'.tr(),
 | 
					 | 
				
			||||||
                              helperText: 'slugHint'.tr(),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                            onTapOutside:
 | 
					 | 
				
			||||||
                                (_) =>
 | 
					 | 
				
			||||||
                                    FocusManager.instance.primaryFocus
 | 
					 | 
				
			||||||
                                        ?.unfocus(),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                          TextFormField(
 | 
					 | 
				
			||||||
                            controller: descriptionController,
 | 
					 | 
				
			||||||
                            decoration: InputDecoration(
 | 
					 | 
				
			||||||
                              labelText: 'description'.tr(),
 | 
					 | 
				
			||||||
                              alignLabelWithHint: true,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                            maxLines: 3,
 | 
					 | 
				
			||||||
                            onTapOutside:
 | 
					 | 
				
			||||||
                                (_) =>
 | 
					 | 
				
			||||||
                                    FocusManager.instance.primaryFocus
 | 
					 | 
				
			||||||
                                        ?.unfocus(),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                          ExpansionPanelList(
 | 
					 | 
				
			||||||
                            expansionCallback: (index, isExpanded) {
 | 
					 | 
				
			||||||
                              switch (index) {
 | 
					 | 
				
			||||||
                                case 0:
 | 
					 | 
				
			||||||
                                  enableLinks.value = isExpanded;
 | 
					 | 
				
			||||||
                                  break;
 | 
					 | 
				
			||||||
                                case 1:
 | 
					 | 
				
			||||||
                                  oauthEnabled.value = isExpanded;
 | 
					 | 
				
			||||||
                                  break;
 | 
					 | 
				
			||||||
                              }
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            children: [
 | 
					 | 
				
			||||||
                              ExpansionPanel(
 | 
					 | 
				
			||||||
                                headerBuilder:
 | 
					 | 
				
			||||||
                                    (context, isExpanded) =>
 | 
					 | 
				
			||||||
                                        ListTile(title: Text('appLinks').tr()),
 | 
					 | 
				
			||||||
                                body: Column(
 | 
					 | 
				
			||||||
                                  spacing: 16,
 | 
					 | 
				
			||||||
                                  children: [
 | 
					 | 
				
			||||||
                                    TextFormField(
 | 
					 | 
				
			||||||
                                      controller: homePageController,
 | 
					 | 
				
			||||||
                                      decoration: InputDecoration(
 | 
					 | 
				
			||||||
                                        labelText: 'homePageUrl'.tr(),
 | 
					 | 
				
			||||||
                                        hintText: 'https://example.com',
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      keyboardType: TextInputType.url,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    TextFormField(
 | 
					 | 
				
			||||||
                                      controller: privacyPolicyController,
 | 
					 | 
				
			||||||
                                      decoration: InputDecoration(
 | 
					 | 
				
			||||||
                                        labelText: 'privacyPolicyUrl'.tr(),
 | 
					 | 
				
			||||||
                                        hintText: 'https://example.com/privacy',
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      keyboardType: TextInputType.url,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    TextFormField(
 | 
					 | 
				
			||||||
                                      controller: termsController,
 | 
					 | 
				
			||||||
                                      decoration: InputDecoration(
 | 
					 | 
				
			||||||
                                        labelText: 'termsOfServiceUrl'.tr(),
 | 
					 | 
				
			||||||
                                        hintText: 'https://example.com/terms',
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      keyboardType: TextInputType.url,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                  ],
 | 
					 | 
				
			||||||
                                ).padding(horizontal: 16, bottom: 24),
 | 
					 | 
				
			||||||
                                isExpanded: enableLinks.value,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                              ExpansionPanel(
 | 
					 | 
				
			||||||
                                headerBuilder:
 | 
					 | 
				
			||||||
                                    (context, isExpanded) => ListTile(
 | 
					 | 
				
			||||||
                                      title: Text('oauthConfig').tr(),
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                body: Column(
 | 
					 | 
				
			||||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                                  children: [
 | 
					 | 
				
			||||||
                                    Text('redirectUris'.tr()),
 | 
					 | 
				
			||||||
                                    Card(
 | 
					 | 
				
			||||||
                                      margin: const EdgeInsets.symmetric(
 | 
					 | 
				
			||||||
                                        vertical: 8,
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      child: Column(
 | 
					 | 
				
			||||||
                                        children: [
 | 
					 | 
				
			||||||
                                          ...redirectUris.value.map(
 | 
					 | 
				
			||||||
                                            (uri) => ListTile(
 | 
					 | 
				
			||||||
                                              title: Text(uri),
 | 
					 | 
				
			||||||
                                              trailing: IconButton(
 | 
					 | 
				
			||||||
                                                icon: const Icon(
 | 
					 | 
				
			||||||
                                                  Symbols.delete,
 | 
					 | 
				
			||||||
                                                ),
 | 
					 | 
				
			||||||
                                                onPressed: () {
 | 
					 | 
				
			||||||
                                                  redirectUris.value =
 | 
					 | 
				
			||||||
                                                      redirectUris.value
 | 
					 | 
				
			||||||
                                                          .where(
 | 
					 | 
				
			||||||
                                                            (u) => u != uri,
 | 
					 | 
				
			||||||
                                                          )
 | 
					 | 
				
			||||||
                                                          .toList();
 | 
					 | 
				
			||||||
                                                },
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                            ),
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                          if (redirectUris.value.isNotEmpty)
 | 
					 | 
				
			||||||
                                            const Divider(height: 1),
 | 
					 | 
				
			||||||
                                          ListTile(
 | 
					 | 
				
			||||||
                                            leading: const Icon(Symbols.add),
 | 
					 | 
				
			||||||
                                            title: Text('addRedirectUri'.tr()),
 | 
					 | 
				
			||||||
                                            onTap: showAddRedirectUriDialog,
 | 
					 | 
				
			||||||
                                            shape: RoundedRectangleBorder(
 | 
					 | 
				
			||||||
                                              borderRadius:
 | 
					 | 
				
			||||||
                                                  BorderRadius.circular(8),
 | 
					 | 
				
			||||||
                                            ),
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ],
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                                    Text('allowedScopes'.tr()),
 | 
					 | 
				
			||||||
                                    Card(
 | 
					 | 
				
			||||||
                                      margin: const EdgeInsets.symmetric(
 | 
					 | 
				
			||||||
                                        vertical: 8,
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      child: Column(
 | 
					 | 
				
			||||||
                                        children: [
 | 
					 | 
				
			||||||
                                          ...allowedScopes.value.map(
 | 
					 | 
				
			||||||
                                            (scope) => ListTile(
 | 
					 | 
				
			||||||
                                              title: Text(scope),
 | 
					 | 
				
			||||||
                                              trailing: IconButton(
 | 
					 | 
				
			||||||
                                                icon: const Icon(
 | 
					 | 
				
			||||||
                                                  Symbols.delete,
 | 
					 | 
				
			||||||
                                                ),
 | 
					 | 
				
			||||||
                                                onPressed: () {
 | 
					 | 
				
			||||||
                                                  allowedScopes.value =
 | 
					 | 
				
			||||||
                                                      allowedScopes.value
 | 
					 | 
				
			||||||
                                                          .where(
 | 
					 | 
				
			||||||
                                                            (s) => s != scope,
 | 
					 | 
				
			||||||
                                                          )
 | 
					 | 
				
			||||||
                                                          .toList();
 | 
					 | 
				
			||||||
                                                },
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                            ),
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                          if (allowedScopes.value.isNotEmpty)
 | 
					 | 
				
			||||||
                                            const Divider(height: 1),
 | 
					 | 
				
			||||||
                                          ListTile(
 | 
					 | 
				
			||||||
                                            leading: const Icon(Symbols.add),
 | 
					 | 
				
			||||||
                                            title: Text('add').tr(),
 | 
					 | 
				
			||||||
                                            onTap: showAddScopeDialog,
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ],
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                                    SwitchListTile(
 | 
					 | 
				
			||||||
                                      title: Text('requirePkce'.tr()),
 | 
					 | 
				
			||||||
                                      value: requirePkce.value,
 | 
					 | 
				
			||||||
                                      onChanged:
 | 
					 | 
				
			||||||
                                          (value) => requirePkce.value = value,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    SwitchListTile(
 | 
					 | 
				
			||||||
                                      title: Text('allowOfflineAccess'.tr()),
 | 
					 | 
				
			||||||
                                      value: allowOfflineAccess.value,
 | 
					 | 
				
			||||||
                                      onChanged:
 | 
					 | 
				
			||||||
                                          (value) =>
 | 
					 | 
				
			||||||
                                              allowOfflineAccess.value = value,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                  ],
 | 
					 | 
				
			||||||
                                ).padding(horizontal: 16, bottom: 24),
 | 
					 | 
				
			||||||
                                isExpanded: oauthEnabled.value,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const SizedBox(height: 16),
 | 
					 | 
				
			||||||
                          Align(
 | 
					 | 
				
			||||||
                            alignment: Alignment.centerRight,
 | 
					 | 
				
			||||||
                            child: TextButton.icon(
 | 
					 | 
				
			||||||
                              onPressed:
 | 
					 | 
				
			||||||
                                  submitting.value ? null : performAction,
 | 
					 | 
				
			||||||
                              label: Text('saveChanges'.tr()),
 | 
					 | 
				
			||||||
                              icon: const Icon(Symbols.save),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ).padding(all: 24),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,20 @@ import 'package:island/screens/developers/edit_app.dart';
 | 
				
			|||||||
class NewCustomAppScreen extends StatelessWidget {
 | 
					class NewCustomAppScreen extends StatelessWidget {
 | 
				
			||||||
  final String publisherName;
 | 
					  final String publisherName;
 | 
				
			||||||
  final String projectId;
 | 
					  final String projectId;
 | 
				
			||||||
  const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
 | 
					  final bool isModal;
 | 
				
			||||||
 | 
					  const NewCustomAppScreen({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.publisherName,
 | 
				
			||||||
 | 
					    required this.projectId,
 | 
				
			||||||
 | 
					    this.isModal = false,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return EditAppScreen(publisherName: publisherName, projectId: projectId);
 | 
					    return EditAppScreen(
 | 
				
			||||||
 | 
					      publisherName: publisherName,
 | 
				
			||||||
 | 
					      projectId: projectId,
 | 
				
			||||||
 | 
					      isModal: isModal,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user