1200 lines
		
	
	
		
			41 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1200 lines
		
	
	
		
			41 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:dio/dio.dart';
 | 
						|
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:go_router/go_router.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/models/post.dart';
 | 
						|
import 'package:island/models/publisher.dart';
 | 
						|
import 'package:island/models/heatmap.dart';
 | 
						|
import 'package:island/pods/network.dart';
 | 
						|
import 'package:island/screens/creators/publishers_form.dart';
 | 
						|
import 'package:island/services/responsive.dart';
 | 
						|
import 'package:island/utils/text.dart';
 | 
						|
import 'package:island/widgets/account/account_picker.dart';
 | 
						|
import 'package:island/widgets/alert.dart';
 | 
						|
import 'package:island/widgets/app_scaffold.dart';
 | 
						|
import 'package:island/widgets/content/cloud_files.dart';
 | 
						|
import 'package:island/widgets/content/sheet.dart';
 | 
						|
import 'package:island/widgets/response.dart';
 | 
						|
import 'package:island/widgets/activity_heatmap.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
						|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
 | 
						|
part 'hub.g.dart';
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
 | 
						|
  if (uname == null) return null;
 | 
						|
  final apiClient = ref.watch(apiClientProvider);
 | 
						|
  final resp = await apiClient.get('/sphere/publishers/$uname/stats');
 | 
						|
  return SnPublisherStats.fromJson(resp.data);
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<SnHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
 | 
						|
  if (uname == null) return null;
 | 
						|
  final apiClient = ref.watch(apiClientProvider);
 | 
						|
  final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
 | 
						|
  return SnHeatmap.fromJson(resp.data);
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
 | 
						|
  try {
 | 
						|
    final apiClient = ref.watch(apiClientProvider);
 | 
						|
    final response = await apiClient.get(
 | 
						|
      '/sphere/publishers/$uname/members/me',
 | 
						|
    );
 | 
						|
    return SnPublisherMember.fromJson(response.data);
 | 
						|
  } catch (err) {
 | 
						|
    if (err is DioException && err.response?.statusCode == 404) {
 | 
						|
      return null; // No identity found, user is not a member
 | 
						|
    }
 | 
						|
    rethrow;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<Map<String, bool>> publisherFeatures(Ref ref, String? uname) async {
 | 
						|
  if (uname == null) return {};
 | 
						|
  final apiClient = ref.watch(apiClientProvider);
 | 
						|
  final response = await apiClient.get('/sphere/publishers/$uname/features');
 | 
						|
  return Map<String, bool>.from(response.data);
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<List<SnPublisherMember>> publisherInvites(Ref ref) async {
 | 
						|
  final client = ref.watch(apiClientProvider);
 | 
						|
  final resp = await client.get('/sphere/publishers/invites');
 | 
						|
  return resp.data
 | 
						|
      .map((e) => SnPublisherMember.fromJson(e))
 | 
						|
      .cast<SnPublisherMember>()
 | 
						|
      .toList();
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
 | 
						|
    with CursorPagingNotifierMixin<SnPublisherMember> {
 | 
						|
  static const int _pageSize = 20;
 | 
						|
 | 
						|
  @override
 | 
						|
  Future<CursorPagingData<SnPublisherMember>> build(String uname) async {
 | 
						|
    return fetch();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Future<CursorPagingData<SnPublisherMember>> fetch({String? cursor}) async {
 | 
						|
    final apiClient = ref.read(apiClientProvider);
 | 
						|
    final offset = cursor != null ? int.parse(cursor) : 0;
 | 
						|
 | 
						|
    final response = await apiClient.get(
 | 
						|
      '/sphere/publishers/$uname/members',
 | 
						|
      queryParameters: {'offset': offset, 'take': _pageSize},
 | 
						|
    );
 | 
						|
 | 
						|
    final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
						|
    final List<dynamic> data = response.data;
 | 
						|
    final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
 | 
						|
 | 
						|
    final hasMore = offset + members.length < total;
 | 
						|
    final nextCursor = hasMore ? (offset + members.length).toString() : null;
 | 
						|
 | 
						|
    return CursorPagingData(
 | 
						|
      items: members,
 | 
						|
      hasMore: hasMore,
 | 
						|
      nextCursor: nextCursor,
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class PublisherSelector extends StatelessWidget {
 | 
						|
  final SnPublisher? currentPublisher;
 | 
						|
  final List<DropdownMenuItem<SnPublisher>> publishersMenu;
 | 
						|
  final ValueChanged<SnPublisher?>? onChanged;
 | 
						|
  final bool isReadOnly;
 | 
						|
 | 
						|
  const PublisherSelector({
 | 
						|
    super.key,
 | 
						|
    required this.currentPublisher,
 | 
						|
    required this.publishersMenu,
 | 
						|
    this.onChanged,
 | 
						|
    this.isReadOnly = false,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    if (isReadOnly || currentPublisher == null) {
 | 
						|
      return ProfilePictureWidget(
 | 
						|
        radius: 16,
 | 
						|
        fileId: currentPublisher?.picture?.id,
 | 
						|
      ).center().padding(right: 8);
 | 
						|
    }
 | 
						|
 | 
						|
    return DropdownButtonHideUnderline(
 | 
						|
      child: DropdownButton2<SnPublisher>(
 | 
						|
        value: currentPublisher,
 | 
						|
        hint: CircleAvatar(
 | 
						|
          radius: 16,
 | 
						|
          child: Icon(
 | 
						|
            Symbols.person,
 | 
						|
            color: Theme.of(
 | 
						|
              context,
 | 
						|
            ).colorScheme.onSecondaryContainer.withOpacity(0.9),
 | 
						|
            fill: 1,
 | 
						|
          ),
 | 
						|
        ).center().padding(right: 8),
 | 
						|
        items: publishersMenu,
 | 
						|
        onChanged: onChanged,
 | 
						|
        selectedItemBuilder: (context) {
 | 
						|
          return publishersMenu
 | 
						|
              .map(
 | 
						|
                (e) => ProfilePictureWidget(
 | 
						|
                  radius: 16,
 | 
						|
                  fileId: e.value?.picture?.id,
 | 
						|
                ).center().padding(right: 8),
 | 
						|
              )
 | 
						|
              .toList();
 | 
						|
        },
 | 
						|
        buttonStyleData: ButtonStyleData(
 | 
						|
          height: 40,
 | 
						|
          padding: const EdgeInsets.only(left: 14, right: 8),
 | 
						|
          decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
 | 
						|
        ),
 | 
						|
        dropdownStyleData: DropdownStyleData(
 | 
						|
          width: 320,
 | 
						|
          padding: const EdgeInsets.symmetric(vertical: 6),
 | 
						|
          decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),
 | 
						|
        ),
 | 
						|
        menuItemStyleData: const MenuItemStyleData(
 | 
						|
          height: 64,
 | 
						|
          padding: EdgeInsets.only(left: 14, right: 14),
 | 
						|
        ),
 | 
						|
        iconStyleData: IconStyleData(
 | 
						|
          icon: Icon(Icons.arrow_drop_down),
 | 
						|
          iconSize: 19,
 | 
						|
          iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
 | 
						|
          iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _PublisherUnselectedWidget extends HookConsumerWidget {
 | 
						|
  final ValueChanged<SnPublisher> onPublisherSelected;
 | 
						|
 | 
						|
  const _PublisherUnselectedWidget({required this.onPublisherSelected});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final publishers = ref.watch(publishersManagedProvider);
 | 
						|
    final publisherInvites = ref.watch(publisherInvitesProvider);
 | 
						|
 | 
						|
    final hasPublishers = publishers.value?.isNotEmpty ?? false;
 | 
						|
 | 
						|
    return Card(
 | 
						|
      margin: const EdgeInsets.all(16),
 | 
						|
      child: Column(
 | 
						|
        children: [
 | 
						|
          if (!hasPublishers) ...[
 | 
						|
            const Icon(
 | 
						|
              Symbols.info,
 | 
						|
              fill: 1,
 | 
						|
              size: 32,
 | 
						|
            ).padding(bottom: 6, top: 24),
 | 
						|
            Text(
 | 
						|
              'creatorHubUnselectedHint',
 | 
						|
              textAlign: TextAlign.center,
 | 
						|
              style: Theme.of(context).textTheme.bodyLarge,
 | 
						|
            ).tr(),
 | 
						|
            const Gap(24),
 | 
						|
          ],
 | 
						|
          if (hasPublishers)
 | 
						|
            ...(publishers.value?.map(
 | 
						|
                  (publisher) => ListTile(
 | 
						|
                    shape: RoundedRectangleBorder(
 | 
						|
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
                    ),
 | 
						|
                    leading: ProfilePictureWidget(file: publisher.picture),
 | 
						|
                    title: Text(publisher.nick),
 | 
						|
                    subtitle: Text('@${publisher.name}'),
 | 
						|
                    onTap: () => onPublisherSelected(publisher),
 | 
						|
                  ),
 | 
						|
                ) ??
 | 
						|
                []),
 | 
						|
          const Divider(height: 1),
 | 
						|
          ListTile(
 | 
						|
            shape: RoundedRectangleBorder(
 | 
						|
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
            ),
 | 
						|
            leading: const CircleAvatar(child: Icon(Symbols.mail)),
 | 
						|
            title: Text('publisherCollabInvitation').tr(),
 | 
						|
            subtitle: Text(
 | 
						|
              'publisherCollabInvitationCount',
 | 
						|
            ).plural(publisherInvites.value?.length ?? 0),
 | 
						|
            trailing: const Icon(Symbols.chevron_right),
 | 
						|
            onTap: () {
 | 
						|
              showModalBottomSheet(
 | 
						|
                context: context,
 | 
						|
                isScrollControlled: true,
 | 
						|
                builder: (_) => const _PublisherInviteSheet(),
 | 
						|
              );
 | 
						|
            },
 | 
						|
          ),
 | 
						|
          ListTile(
 | 
						|
            shape: RoundedRectangleBorder(
 | 
						|
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
            ),
 | 
						|
            leading: const CircleAvatar(child: Icon(Symbols.add)),
 | 
						|
            title: Text('createPublisher').tr(),
 | 
						|
            subtitle: Text('createPublisherHint').tr(),
 | 
						|
            trailing: const Icon(Symbols.chevron_right),
 | 
						|
            onTap: () {
 | 
						|
              context.pushNamed('creatorNew').then((value) {
 | 
						|
                if (value != null) {
 | 
						|
                  ref.invalidate(publishersManagedProvider);
 | 
						|
                }
 | 
						|
              });
 | 
						|
            },
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class CreatorHubScreen extends HookConsumerWidget {
 | 
						|
  const CreatorHubScreen({super.key});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final publishers = ref.watch(publishersManagedProvider);
 | 
						|
    final currentPublisher = useState<SnPublisher?>(
 | 
						|
      publishers.value?.firstOrNull,
 | 
						|
    );
 | 
						|
 | 
						|
    void updatePublisher() {
 | 
						|
      context
 | 
						|
          .pushNamed(
 | 
						|
            'creatorEdit',
 | 
						|
            pathParameters: {'name': currentPublisher.value!.name},
 | 
						|
          )
 | 
						|
          .then((value) async {
 | 
						|
            if (value == null) return;
 | 
						|
            final data = await ref.refresh(publishersManagedProvider.future);
 | 
						|
            currentPublisher.value =
 | 
						|
                data
 | 
						|
                    .where((e) => e.id == currentPublisher.value!.id)
 | 
						|
                    .firstOrNull;
 | 
						|
          });
 | 
						|
    }
 | 
						|
 | 
						|
    void deletePublisher() {
 | 
						|
      showConfirmAlert('deletePublisherHint'.tr(), 'deletePublisher'.tr()).then(
 | 
						|
        (confirm) {
 | 
						|
          if (confirm) {
 | 
						|
            final client = ref.watch(apiClientProvider);
 | 
						|
            client.delete('/sphere/publishers/${currentPublisher.value!.name}');
 | 
						|
            ref.invalidate(publishersManagedProvider);
 | 
						|
            currentPublisher.value = null;
 | 
						|
          }
 | 
						|
        },
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
 | 
						|
      data:
 | 
						|
          (data) =>
 | 
						|
              data
 | 
						|
                  .map(
 | 
						|
                    (item) => DropdownMenuItem<SnPublisher>(
 | 
						|
                      value: item,
 | 
						|
                      child: ListTile(
 | 
						|
                        minTileHeight: 48,
 | 
						|
                        leading: ProfilePictureWidget(
 | 
						|
                          radius: 16,
 | 
						|
                          fileId: item.picture?.id,
 | 
						|
                        ),
 | 
						|
                        title: Text(item.nick),
 | 
						|
                        subtitle: Text('@${item.name}'),
 | 
						|
                        trailing:
 | 
						|
                            currentPublisher.value?.id == item.id
 | 
						|
                                ? const Icon(Icons.check)
 | 
						|
                                : null,
 | 
						|
                        contentPadding: EdgeInsets.symmetric(horizontal: 8),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  )
 | 
						|
                  .toList(),
 | 
						|
      loading: () => [],
 | 
						|
      error: (_, _) => [],
 | 
						|
    );
 | 
						|
 | 
						|
    final publisherStats = ref.watch(
 | 
						|
      publisherStatsProvider(currentPublisher.value?.name),
 | 
						|
    );
 | 
						|
 | 
						|
    final publisherHeatmap = ref.watch(
 | 
						|
      publisherHeatmapProvider(currentPublisher.value?.name),
 | 
						|
    );
 | 
						|
 | 
						|
    final publisherFeatures = ref.watch(
 | 
						|
      publisherFeaturesProvider(currentPublisher.value?.name),
 | 
						|
    );
 | 
						|
 | 
						|
    Widget buildNavigationWidget(bool isWide) {
 | 
						|
      final leftItems = [
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('stickers').tr(),
 | 
						|
          trailing: Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.ar_stickers),
 | 
						|
          onTap: () {
 | 
						|
            context.pushNamed(
 | 
						|
              'creatorStickers',
 | 
						|
              pathParameters: {'name': currentPublisher.value!.name},
 | 
						|
            );
 | 
						|
          },
 | 
						|
        ),
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('posts').tr(),
 | 
						|
          trailing: Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.sticky_note_2),
 | 
						|
          onTap: () {
 | 
						|
            context.pushNamed(
 | 
						|
              'creatorPosts',
 | 
						|
              pathParameters: {'name': currentPublisher.value!.name},
 | 
						|
            );
 | 
						|
          },
 | 
						|
        ),
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('polls').tr(),
 | 
						|
          trailing: const Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.poll),
 | 
						|
          onTap: () {
 | 
						|
            context.pushNamed(
 | 
						|
              'creatorPolls',
 | 
						|
              pathParameters: {'name': currentPublisher.value!.name},
 | 
						|
            );
 | 
						|
          },
 | 
						|
        ),
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: const Text('webFeeds').tr(),
 | 
						|
          trailing: const Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.rss_feed),
 | 
						|
          onTap: () {
 | 
						|
            context.push('/creators/${currentPublisher.value!.name}/feeds');
 | 
						|
          },
 | 
						|
        ),
 | 
						|
      ];
 | 
						|
 | 
						|
      final rightItems = [
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('publisherMembers').tr(),
 | 
						|
          trailing: const Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.group),
 | 
						|
          onTap: () {
 | 
						|
            showModalBottomSheet(
 | 
						|
              isScrollControlled: true,
 | 
						|
              context: context,
 | 
						|
              builder:
 | 
						|
                  (context) => _PublisherMemberListSheet(
 | 
						|
                    publisherUname: currentPublisher.value!.name,
 | 
						|
                  ),
 | 
						|
            );
 | 
						|
          },
 | 
						|
        ),
 | 
						|
        ExpansionTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          title: Text('publisherFeatures').tr(),
 | 
						|
          leading: const Icon(Symbols.flag),
 | 
						|
          tilePadding: const EdgeInsets.only(left: 16, right: 24),
 | 
						|
          minTileHeight: 48,
 | 
						|
          children: [
 | 
						|
            ...publisherFeatures.when(
 | 
						|
              data: (data) {
 | 
						|
                return data.entries.map((entry) {
 | 
						|
                  final keyPrefix =
 | 
						|
                      'publisherFeature${entry.key.capitalizeEachWord()}';
 | 
						|
                  return ListTile(
 | 
						|
                    minTileHeight: 48,
 | 
						|
                    contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
						|
                    leading: Icon(
 | 
						|
                      Symbols.circle,
 | 
						|
                      color: entry.value ? Colors.green : Colors.red,
 | 
						|
                      fill: 1,
 | 
						|
                      size: 16,
 | 
						|
                    ).padding(left: 2, top: 4),
 | 
						|
                    title: Text(keyPrefix).tr(),
 | 
						|
                    subtitle: Column(
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                        Text('${keyPrefix}Description').tr(),
 | 
						|
                        if (!entry.value) Text('${keyPrefix}Hint').tr().bold(),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                    isThreeLine: true,
 | 
						|
                  );
 | 
						|
                }).toList();
 | 
						|
              },
 | 
						|
              error: (_, _) => [],
 | 
						|
              loading: () => [],
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('editPublisher').tr(),
 | 
						|
          trailing: Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.edit),
 | 
						|
          onTap: updatePublisher,
 | 
						|
        ),
 | 
						|
        ListTile(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
						|
          ),
 | 
						|
          minTileHeight: 48,
 | 
						|
          title: Text('deletePublisher').tr(),
 | 
						|
          trailing: Icon(Symbols.chevron_right),
 | 
						|
          leading: const Icon(Symbols.delete),
 | 
						|
          onTap: deletePublisher,
 | 
						|
        ),
 | 
						|
      ];
 | 
						|
 | 
						|
      if (isWide) {
 | 
						|
        return Row(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          spacing: 8,
 | 
						|
          children: [
 | 
						|
            Expanded(
 | 
						|
              child: Card(
 | 
						|
                margin: EdgeInsets.zero,
 | 
						|
                child: Column(children: leftItems),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            Expanded(
 | 
						|
              child: Card(
 | 
						|
                margin: EdgeInsets.zero,
 | 
						|
                child: Column(children: rightItems),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ).padding(horizontal: 12);
 | 
						|
      } else {
 | 
						|
        return Card(
 | 
						|
          margin: const EdgeInsets.symmetric(horizontal: 16),
 | 
						|
          child: Column(
 | 
						|
            children: [...leftItems, const Divider(height: 8), ...rightItems],
 | 
						|
          ),
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return AppScaffold(
 | 
						|
      isNoBackground: false,
 | 
						|
      appBar: AppBar(
 | 
						|
        leading: const PageBackButton(backTo: '/account'),
 | 
						|
        title: Text('creatorHub').tr(),
 | 
						|
        actions: [
 | 
						|
          if (!isWideScreen(context))
 | 
						|
            PublisherSelector(
 | 
						|
              currentPublisher: currentPublisher.value,
 | 
						|
              publishersMenu: publishersMenu,
 | 
						|
              onChanged: (value) {
 | 
						|
                currentPublisher.value = value;
 | 
						|
              },
 | 
						|
            ),
 | 
						|
          const Gap(8),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
      body: LayoutBuilder(
 | 
						|
        builder: (context, constraints) {
 | 
						|
          final isWide = isWideScreen(context);
 | 
						|
          final maxWidth = isWide ? 800.0 : double.infinity;
 | 
						|
 | 
						|
          return Center(
 | 
						|
            child: ConstrainedBox(
 | 
						|
              constraints: BoxConstraints(maxWidth: maxWidth),
 | 
						|
              child: publisherStats.when(
 | 
						|
                data:
 | 
						|
                    (stats) => SingleChildScrollView(
 | 
						|
                      child:
 | 
						|
                          currentPublisher.value == null
 | 
						|
                              ? ConstrainedBox(
 | 
						|
                                constraints: BoxConstraints(maxWidth: 640),
 | 
						|
                                child: _PublisherUnselectedWidget(
 | 
						|
                                  onPublisherSelected: (publisher) {
 | 
						|
                                    currentPublisher.value = publisher;
 | 
						|
                                  },
 | 
						|
                                ),
 | 
						|
                              ).center()
 | 
						|
                              : isWide
 | 
						|
                              ? Column(
 | 
						|
                                spacing: 8,
 | 
						|
                                children: [
 | 
						|
                                  PublisherSelector(
 | 
						|
                                    currentPublisher: currentPublisher.value,
 | 
						|
                                    publishersMenu: publishersMenu,
 | 
						|
                                    onChanged: (value) {
 | 
						|
                                      currentPublisher.value = value;
 | 
						|
                                    },
 | 
						|
                                  ),
 | 
						|
                                  if (stats != null)
 | 
						|
                                    _PublisherStatsWidget(
 | 
						|
                                      stats: stats,
 | 
						|
                                      heatmap: publisherHeatmap.value,
 | 
						|
                                    ).padding(horizontal: 12),
 | 
						|
                                  buildNavigationWidget(true),
 | 
						|
                                ],
 | 
						|
                              )
 | 
						|
                              : Column(
 | 
						|
                                spacing: 12,
 | 
						|
                                children: [
 | 
						|
                                  if (stats != null)
 | 
						|
                                    _PublisherStatsWidget(
 | 
						|
                                      stats: stats,
 | 
						|
                                      heatmap: publisherHeatmap.value,
 | 
						|
                                    ).padding(horizontal: 16),
 | 
						|
                                  buildNavigationWidget(false),
 | 
						|
                                ],
 | 
						|
                              ),
 | 
						|
                    ),
 | 
						|
                loading: () => const Center(child: CircularProgressIndicator()),
 | 
						|
                error: (_, _) => const SizedBox.shrink(),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          );
 | 
						|
        },
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _PublisherStatsWidget extends StatelessWidget {
 | 
						|
  final SnPublisherStats stats;
 | 
						|
  final SnHeatmap? heatmap;
 | 
						|
  const _PublisherStatsWidget({required this.stats, this.heatmap});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return SingleChildScrollView(
 | 
						|
      child: Column(
 | 
						|
        spacing: 8,
 | 
						|
        children: [
 | 
						|
          Row(
 | 
						|
            spacing: 8,
 | 
						|
            children: [
 | 
						|
              Expanded(
 | 
						|
                child: _buildStatsCard(
 | 
						|
                  context,
 | 
						|
                  stats.postsCreated.toString(),
 | 
						|
                  'postsCreatedCount',
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              Expanded(
 | 
						|
                child: _buildStatsCard(
 | 
						|
                  context,
 | 
						|
                  stats.stickerPacksCreated.toString(),
 | 
						|
                  'stickerPacksCreatedCount',
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              Expanded(
 | 
						|
                child: _buildStatsCard(
 | 
						|
                  context,
 | 
						|
                  stats.stickersCreated.toString(),
 | 
						|
                  'stickersCreatedCount',
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
          Row(
 | 
						|
            spacing: 8,
 | 
						|
            children: [
 | 
						|
              Expanded(
 | 
						|
                child: _buildStatsCard(
 | 
						|
                  context,
 | 
						|
                  stats.upvoteReceived.toString(),
 | 
						|
                  'upvoteReceived',
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              Expanded(
 | 
						|
                child: _buildStatsCard(
 | 
						|
                  context,
 | 
						|
                  stats.downvoteReceived.toString(),
 | 
						|
                  'downvoteReceived',
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
          if (heatmap != null) ActivityHeatmapWidget(heatmap: heatmap!),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildStatsCard(
 | 
						|
    BuildContext context,
 | 
						|
    String statValue,
 | 
						|
    String statLabel,
 | 
						|
  ) {
 | 
						|
    return Card(
 | 
						|
      margin: EdgeInsets.zero,
 | 
						|
      child: SizedBox(
 | 
						|
        height: 100,
 | 
						|
        child: Padding(
 | 
						|
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
 | 
						|
          child: Tooltip(
 | 
						|
            richMessage: TextSpan(
 | 
						|
              text: statLabel.tr(),
 | 
						|
              style: TextStyle(fontWeight: FontWeight.bold),
 | 
						|
              children: [
 | 
						|
                TextSpan(text: ' '),
 | 
						|
                TextSpan(
 | 
						|
                  text: statValue,
 | 
						|
                  style: TextStyle(fontWeight: FontWeight.normal),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            child: Column(
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
              mainAxisAlignment: MainAxisAlignment.center,
 | 
						|
              children: [
 | 
						|
                Text(
 | 
						|
                  statValue,
 | 
						|
                  style: Theme.of(context).textTheme.headlineMedium,
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                ),
 | 
						|
                const Gap(4),
 | 
						|
                Text(
 | 
						|
                  statLabel,
 | 
						|
                  maxLines: 1,
 | 
						|
                  overflow: TextOverflow.ellipsis,
 | 
						|
                ).tr(),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class PublisherMemberState {
 | 
						|
  final List<SnPublisherMember> members;
 | 
						|
  final bool isLoading;
 | 
						|
  final int total;
 | 
						|
  final String? error;
 | 
						|
 | 
						|
  const PublisherMemberState({
 | 
						|
    required this.members,
 | 
						|
    required this.isLoading,
 | 
						|
    required this.total,
 | 
						|
    this.error,
 | 
						|
  });
 | 
						|
 | 
						|
  PublisherMemberState copyWith({
 | 
						|
    List<SnPublisherMember>? members,
 | 
						|
    bool? isLoading,
 | 
						|
    int? total,
 | 
						|
    String? error,
 | 
						|
  }) {
 | 
						|
    return PublisherMemberState(
 | 
						|
      members: members ?? this.members,
 | 
						|
      isLoading: isLoading ?? this.isLoading,
 | 
						|
      total: total ?? this.total,
 | 
						|
      error: error ?? this.error,
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
final publisherMemberStateProvider = StateNotifierProvider.family<
 | 
						|
  PublisherMemberNotifier,
 | 
						|
  PublisherMemberState,
 | 
						|
  String
 | 
						|
>((ref, publisherUname) {
 | 
						|
  final apiClient = ref.watch(apiClientProvider);
 | 
						|
  return PublisherMemberNotifier(apiClient, publisherUname);
 | 
						|
});
 | 
						|
 | 
						|
class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
 | 
						|
  final String publisherUname;
 | 
						|
  final Dio _apiClient;
 | 
						|
 | 
						|
  PublisherMemberNotifier(this._apiClient, this.publisherUname)
 | 
						|
    : super(
 | 
						|
        const PublisherMemberState(members: [], isLoading: false, total: 0),
 | 
						|
      );
 | 
						|
 | 
						|
  Future<void> loadMore({int offset = 0, int take = 20}) async {
 | 
						|
    if (state.isLoading) return;
 | 
						|
    if (state.total > 0 && state.members.length >= state.total) return;
 | 
						|
 | 
						|
    state = state.copyWith(isLoading: true, error: null);
 | 
						|
 | 
						|
    try {
 | 
						|
      final response = await _apiClient.get(
 | 
						|
        '/sphere/publishers/$publisherUname/members',
 | 
						|
        queryParameters: {'offset': offset, 'take': take},
 | 
						|
      );
 | 
						|
 | 
						|
      final total = int.parse(response.headers.value('X-Total') ?? '0');
 | 
						|
      final List<dynamic> data = response.data;
 | 
						|
      final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
 | 
						|
 | 
						|
      state = state.copyWith(
 | 
						|
        members: [...state.members, ...members],
 | 
						|
        total: total,
 | 
						|
        isLoading: false,
 | 
						|
      );
 | 
						|
    } catch (e) {
 | 
						|
      state = state.copyWith(error: e.toString(), isLoading: false);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void reset() {
 | 
						|
    state = const PublisherMemberState(members: [], isLoading: false, total: 0);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _PublisherMemberListSheet extends HookConsumerWidget {
 | 
						|
  final String publisherUname;
 | 
						|
  const _PublisherMemberListSheet({required this.publisherUname});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final publisherIdentity = ref.watch(
 | 
						|
      publisherIdentityProvider(publisherUname),
 | 
						|
    );
 | 
						|
    final memberListProvider = publisherMemberListNotifierProvider(
 | 
						|
      publisherUname,
 | 
						|
    );
 | 
						|
    final memberState = ref.watch(publisherMemberStateProvider(publisherUname));
 | 
						|
    final memberNotifier = ref.read(
 | 
						|
      publisherMemberStateProvider(publisherUname).notifier,
 | 
						|
    );
 | 
						|
 | 
						|
    useEffect(() {
 | 
						|
      Future(() {
 | 
						|
        memberNotifier.loadMore();
 | 
						|
      });
 | 
						|
      return null;
 | 
						|
    }, []);
 | 
						|
 | 
						|
    Future<void> invitePerson() async {
 | 
						|
      final result = await showModalBottomSheet(
 | 
						|
        useRootNavigator: true,
 | 
						|
        isScrollControlled: true,
 | 
						|
        context: context,
 | 
						|
        builder: (context) => const AccountPickerSheet(),
 | 
						|
      );
 | 
						|
      if (result == null) return;
 | 
						|
      try {
 | 
						|
        final apiClient = ref.watch(apiClientProvider);
 | 
						|
        await apiClient.post(
 | 
						|
          '/publishers/$publisherUname/invites',
 | 
						|
          data: {'related_user_id': result.id, 'role': 0},
 | 
						|
        );
 | 
						|
        // Refresh both providers
 | 
						|
        memberNotifier.reset();
 | 
						|
        await memberNotifier.loadMore();
 | 
						|
        ref.invalidate(memberListProvider);
 | 
						|
      } catch (err) {
 | 
						|
        showErrorAlert(err);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return Container(
 | 
						|
      constraints: BoxConstraints(
 | 
						|
        maxHeight: MediaQuery.of(context).size.height * 0.8,
 | 
						|
      ),
 | 
						|
      child: Column(
 | 
						|
        children: [
 | 
						|
          Padding(
 | 
						|
            padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
 | 
						|
            child: Row(
 | 
						|
              children: [
 | 
						|
                Text(
 | 
						|
                  'members'.plural(memberState.total),
 | 
						|
                  style: Theme.of(context).textTheme.headlineSmall?.copyWith(
 | 
						|
                    fontWeight: FontWeight.w600,
 | 
						|
                    letterSpacing: -0.5,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                const Spacer(),
 | 
						|
                IconButton(
 | 
						|
                  icon: const Icon(Symbols.person_add),
 | 
						|
                  onPressed: invitePerson,
 | 
						|
                  style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
 | 
						|
                ),
 | 
						|
                IconButton(
 | 
						|
                  icon: const Icon(Symbols.refresh),
 | 
						|
                  onPressed: () {
 | 
						|
                    memberNotifier.reset();
 | 
						|
                    memberNotifier.loadMore();
 | 
						|
                    ref.invalidate(memberListProvider);
 | 
						|
                  },
 | 
						|
                ),
 | 
						|
                IconButton(
 | 
						|
                  icon: const Icon(Symbols.close),
 | 
						|
                  onPressed: () => Navigator.pop(context),
 | 
						|
                  style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          const Divider(height: 1),
 | 
						|
          Expanded(
 | 
						|
            child: PagingHelperView(
 | 
						|
              provider: memberListProvider,
 | 
						|
              futureRefreshable: memberListProvider.future,
 | 
						|
              notifierRefreshable: memberListProvider.notifier,
 | 
						|
              contentBuilder: (data, widgetCount, endItemView) {
 | 
						|
                return ListView.builder(
 | 
						|
                  itemCount: widgetCount,
 | 
						|
                  itemBuilder: (context, index) {
 | 
						|
                    if (index == data.items.length) {
 | 
						|
                      return endItemView;
 | 
						|
                    }
 | 
						|
 | 
						|
                    final member = data.items[index];
 | 
						|
                    return ListTile(
 | 
						|
                      contentPadding: EdgeInsets.only(left: 16, right: 12),
 | 
						|
                      leading: ProfilePictureWidget(
 | 
						|
                        fileId: member.account!.profile.picture?.id,
 | 
						|
                      ),
 | 
						|
                      title: Row(
 | 
						|
                        spacing: 6,
 | 
						|
                        children: [
 | 
						|
                          Flexible(child: Text(member.account!.nick)),
 | 
						|
                          if (member.joinedAt == null)
 | 
						|
                            const Icon(Symbols.pending_actions, size: 20),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      subtitle: Row(
 | 
						|
                        children: [
 | 
						|
                          Text(
 | 
						|
                            member.role >= 100
 | 
						|
                                ? 'permissionOwner'
 | 
						|
                                : member.role >= 50
 | 
						|
                                ? 'permissionModerator'
 | 
						|
                                : 'permissionMember',
 | 
						|
                          ).tr(),
 | 
						|
                          Text('·').bold().padding(horizontal: 6),
 | 
						|
                          Expanded(child: Text("@${member.account!.name}")),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      trailing: Row(
 | 
						|
                        mainAxisSize: MainAxisSize.min,
 | 
						|
                        children: [
 | 
						|
                          if ((publisherIdentity.value?.role ?? 0) >= 50)
 | 
						|
                            IconButton(
 | 
						|
                              icon: const Icon(Symbols.edit),
 | 
						|
                              onPressed: () {
 | 
						|
                                showModalBottomSheet(
 | 
						|
                                  isScrollControlled: true,
 | 
						|
                                  context: context,
 | 
						|
                                  builder:
 | 
						|
                                      (context) => _PublisherMemberRoleSheet(
 | 
						|
                                        publisherUname: publisherUname,
 | 
						|
                                        member: member,
 | 
						|
                                      ),
 | 
						|
                                ).then((value) {
 | 
						|
                                  if (value != null) {
 | 
						|
                                    // Refresh both providers
 | 
						|
                                    memberNotifier.reset();
 | 
						|
                                    memberNotifier.loadMore();
 | 
						|
                                    ref.invalidate(memberListProvider);
 | 
						|
                                  }
 | 
						|
                                });
 | 
						|
                              },
 | 
						|
                            ),
 | 
						|
                          if ((publisherIdentity.value?.role ?? 0) >= 50)
 | 
						|
                            IconButton(
 | 
						|
                              icon: const Icon(Symbols.delete),
 | 
						|
                              onPressed: () {
 | 
						|
                                showConfirmAlert(
 | 
						|
                                  'removePublisherMemberHint'.tr(),
 | 
						|
                                  'removePublisherMember'.tr(),
 | 
						|
                                ).then((confirm) async {
 | 
						|
                                  if (confirm != true) return;
 | 
						|
                                  try {
 | 
						|
                                    final apiClient = ref.watch(
 | 
						|
                                      apiClientProvider,
 | 
						|
                                    );
 | 
						|
                                    await apiClient.delete(
 | 
						|
                                      '/publishers/$publisherUname/members/${member.accountId}',
 | 
						|
                                    );
 | 
						|
                                    // Refresh both providers
 | 
						|
                                    memberNotifier.reset();
 | 
						|
                                    memberNotifier.loadMore();
 | 
						|
                                    ref.invalidate(memberListProvider);
 | 
						|
                                  } catch (err) {
 | 
						|
                                    showErrorAlert(err);
 | 
						|
                                  }
 | 
						|
                                });
 | 
						|
                              },
 | 
						|
                            ),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                    );
 | 
						|
                  },
 | 
						|
                );
 | 
						|
              },
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _PublisherMemberRoleSheet extends HookConsumerWidget {
 | 
						|
  final String publisherUname;
 | 
						|
  final SnPublisherMember member;
 | 
						|
 | 
						|
  const _PublisherMemberRoleSheet({
 | 
						|
    required this.publisherUname,
 | 
						|
    required this.member,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final roleController = useTextEditingController(
 | 
						|
      text: member.role.toString(),
 | 
						|
    );
 | 
						|
 | 
						|
    return Container(
 | 
						|
      padding: EdgeInsets.only(
 | 
						|
        bottom: MediaQuery.of(context).viewInsets.bottom,
 | 
						|
      ),
 | 
						|
      child: SafeArea(
 | 
						|
        child: Column(
 | 
						|
          mainAxisSize: MainAxisSize.min,
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
          children: [
 | 
						|
            Padding(
 | 
						|
              padding: EdgeInsets.only(
 | 
						|
                top: 16,
 | 
						|
                left: 20,
 | 
						|
                right: 16,
 | 
						|
                bottom: 12,
 | 
						|
              ),
 | 
						|
              child: Row(
 | 
						|
                children: [
 | 
						|
                  Text(
 | 
						|
                    'memberRoleEdit'.tr(args: [member.account!.name]),
 | 
						|
                    style: Theme.of(context).textTheme.headlineSmall?.copyWith(
 | 
						|
                      fontWeight: FontWeight.w600,
 | 
						|
                      letterSpacing: -0.5,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  const Spacer(),
 | 
						|
                  IconButton(
 | 
						|
                    icon: const Icon(Symbols.close),
 | 
						|
                    onPressed: () => Navigator.pop(context),
 | 
						|
                    style: IconButton.styleFrom(
 | 
						|
                      minimumSize: const Size(36, 36),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            const Divider(height: 1),
 | 
						|
            Column(
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
						|
              children: [
 | 
						|
                Autocomplete<int>(
 | 
						|
                  optionsBuilder: (TextEditingValue textEditingValue) {
 | 
						|
                    if (textEditingValue.text.isEmpty) {
 | 
						|
                      return const [100, 50, 0];
 | 
						|
                    }
 | 
						|
                    final int? value = int.tryParse(textEditingValue.text);
 | 
						|
                    if (value == null) return const [100, 50, 0];
 | 
						|
                    return [100, 50, 0].where(
 | 
						|
                      (option) =>
 | 
						|
                          option.toString().contains(textEditingValue.text),
 | 
						|
                    );
 | 
						|
                  },
 | 
						|
                  onSelected: (int selection) {
 | 
						|
                    roleController.text = selection.toString();
 | 
						|
                  },
 | 
						|
                  fieldViewBuilder: (
 | 
						|
                    context,
 | 
						|
                    controller,
 | 
						|
                    focusNode,
 | 
						|
                    onFieldSubmitted,
 | 
						|
                  ) {
 | 
						|
                    return TextField(
 | 
						|
                      controller: controller,
 | 
						|
                      focusNode: focusNode,
 | 
						|
                      keyboardType: TextInputType.number,
 | 
						|
                      decoration: InputDecoration(
 | 
						|
                        labelText: 'memberRole'.tr(),
 | 
						|
                        helperText: 'memberRoleHint'.tr(),
 | 
						|
                      ),
 | 
						|
                      onTapOutside: (event) => focusNode.unfocus(),
 | 
						|
                    );
 | 
						|
                  },
 | 
						|
                ),
 | 
						|
                const Gap(16),
 | 
						|
                FilledButton.icon(
 | 
						|
                  onPressed: () async {
 | 
						|
                    try {
 | 
						|
                      final newRole = int.parse(roleController.text);
 | 
						|
                      if (newRole < 0 || newRole > 100) {
 | 
						|
                        throw 'Role must be between 0 and 100';
 | 
						|
                      }
 | 
						|
 | 
						|
                      final apiClient = ref.read(apiClientProvider);
 | 
						|
                      await apiClient.patch(
 | 
						|
                        '/publishers/$publisherUname/members/${member.accountId}/role',
 | 
						|
                        data: newRole,
 | 
						|
                      );
 | 
						|
 | 
						|
                      if (context.mounted) Navigator.pop(context, true);
 | 
						|
                    } catch (err) {
 | 
						|
                      showErrorAlert(err);
 | 
						|
                    }
 | 
						|
                  },
 | 
						|
                  icon: const Icon(Symbols.save),
 | 
						|
                  label: const Text('saveChanges').tr(),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ).padding(vertical: 16, horizontal: 24),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _PublisherInviteSheet extends HookConsumerWidget {
 | 
						|
  const _PublisherInviteSheet();
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final invites = ref.watch(publisherInvitesProvider);
 | 
						|
 | 
						|
    Future<void> acceptInvite(SnPublisherMember invite) async {
 | 
						|
      try {
 | 
						|
        final client = ref.read(apiClientProvider);
 | 
						|
        await client.post(
 | 
						|
          '/publishers/invites/${invite.publisher!.name}/accept',
 | 
						|
        );
 | 
						|
        ref.invalidate(publisherInvitesProvider);
 | 
						|
        ref.invalidate(publishersManagedProvider);
 | 
						|
      } catch (err) {
 | 
						|
        showErrorAlert(err);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    Future<void> declineInvite(SnPublisherMember invite) async {
 | 
						|
      try {
 | 
						|
        final client = ref.read(apiClientProvider);
 | 
						|
        await client.post(
 | 
						|
          '/publishers/invites/${invite.publisher!.name}/decline',
 | 
						|
        );
 | 
						|
        ref.invalidate(publisherInvitesProvider);
 | 
						|
      } catch (err) {
 | 
						|
        showErrorAlert(err);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return SheetScaffold(
 | 
						|
      titleText: 'invites'.tr(),
 | 
						|
      actions: [
 | 
						|
        IconButton(
 | 
						|
          icon: const Icon(Symbols.refresh),
 | 
						|
          style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
 | 
						|
          onPressed: () {
 | 
						|
            ref.invalidate(publisherInvitesProvider);
 | 
						|
          },
 | 
						|
        ),
 | 
						|
      ],
 | 
						|
      child: invites.when(
 | 
						|
        data:
 | 
						|
            (items) =>
 | 
						|
                items.isEmpty
 | 
						|
                    ? Center(
 | 
						|
                      child:
 | 
						|
                          Text(
 | 
						|
                            'invitesEmpty',
 | 
						|
                            textAlign: TextAlign.center,
 | 
						|
                          ).tr(),
 | 
						|
                    )
 | 
						|
                    : ListView.builder(
 | 
						|
                      shrinkWrap: true,
 | 
						|
                      itemCount: items.length,
 | 
						|
                      itemBuilder: (context, index) {
 | 
						|
                        final invite = items[index];
 | 
						|
                        return ListTile(
 | 
						|
                          leading: ProfilePictureWidget(
 | 
						|
                            fileId: invite.publisher!.picture?.id,
 | 
						|
                            fallbackIcon: Symbols.group,
 | 
						|
                          ),
 | 
						|
                          title: Text(invite.publisher!.nick),
 | 
						|
                          subtitle:
 | 
						|
                              Text(
 | 
						|
                                invite.role >= 100
 | 
						|
                                    ? 'permissionOwner'
 | 
						|
                                    : invite.role >= 50
 | 
						|
                                    ? 'permissionModerator'
 | 
						|
                                    : 'permissionMember',
 | 
						|
                              ).tr(),
 | 
						|
                          trailing: Row(
 | 
						|
                            mainAxisSize: MainAxisSize.min,
 | 
						|
                            children: [
 | 
						|
                              IconButton(
 | 
						|
                                icon: const Icon(Symbols.check),
 | 
						|
                                onPressed: () => acceptInvite(invite),
 | 
						|
                              ),
 | 
						|
                              IconButton(
 | 
						|
                                icon: const Icon(Symbols.close),
 | 
						|
                                onPressed: () => declineInvite(invite),
 | 
						|
                              ),
 | 
						|
                            ],
 | 
						|
                          ),
 | 
						|
                        );
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
        loading: () => const Center(child: CircularProgressIndicator()),
 | 
						|
        error:
 | 
						|
            (error, _) => ResponseErrorWidget(
 | 
						|
              error: error,
 | 
						|
              onRetry: () => ref.invalidate(publisherInvitesProvider),
 | 
						|
            ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |