283 lines
8.8 KiB
Dart
283 lines
8.8 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:gap/gap.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:island/models/sticker.dart';
|
|
import 'package:island/pods/network.dart';
|
|
import 'package:island/route.gr.dart';
|
|
import 'package:island/widgets/alert.dart';
|
|
import 'package:island/widgets/app_scaffold.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
|
|
|
part 'stickers.g.dart';
|
|
|
|
@RoutePage()
|
|
class StickersScreen extends HookConsumerWidget {
|
|
final String pubName;
|
|
const StickersScreen({super.key, @PathParam("name") required this.pubName});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return AppScaffold(
|
|
appBar: AppBar(
|
|
title: const Text('stickers').tr(),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () {
|
|
context.router.push(NewStickerPacksRoute(pubName: pubName)).then((
|
|
value,
|
|
) {
|
|
if (value != null) {
|
|
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
|
}
|
|
});
|
|
},
|
|
icon: const Icon(Symbols.add_circle),
|
|
),
|
|
const Gap(8),
|
|
],
|
|
),
|
|
body: SliverStickerPacksList(pubName: pubName),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SliverStickerPacksList extends HookConsumerWidget {
|
|
final String pubName;
|
|
const SliverStickerPacksList({super.key, required this.pubName});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return PagingHelperView(
|
|
provider: stickerPacksNotifierProvider(pubName),
|
|
futureRefreshable: stickerPacksNotifierProvider(pubName).future,
|
|
notifierRefreshable: stickerPacksNotifierProvider(pubName).notifier,
|
|
contentBuilder:
|
|
(data, widgetCount, endItemView) => ListView.builder(
|
|
padding: EdgeInsets.zero,
|
|
itemCount: widgetCount,
|
|
itemBuilder: (context, index) {
|
|
if (index == widgetCount - 1) {
|
|
return endItemView;
|
|
}
|
|
|
|
final sticker = data.items[index];
|
|
return ListTile(
|
|
title: Text(sticker.name),
|
|
subtitle: Text(sticker.description),
|
|
trailing: const Icon(Symbols.chevron_right),
|
|
onTap: () {
|
|
context.router.push(
|
|
StickerPackDetailRoute(pubName: pubName, id: sticker.id),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@riverpod
|
|
class StickerPacksNotifier extends _$StickerPacksNotifier
|
|
with CursorPagingNotifierMixin<SnStickerPack> {
|
|
static const int _pageSize = 20;
|
|
|
|
@override
|
|
Future<CursorPagingData<SnStickerPack>> build(String pubName) {
|
|
return fetch(cursor: null);
|
|
}
|
|
|
|
@override
|
|
Future<CursorPagingData<SnStickerPack>> fetch({
|
|
required String? cursor,
|
|
}) async {
|
|
final client = ref.read(apiClientProvider);
|
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
|
|
|
try {
|
|
final response = await client.get(
|
|
'/stickers',
|
|
queryParameters: {
|
|
'offset': offset,
|
|
'take': _pageSize,
|
|
'pubName': pubName,
|
|
},
|
|
);
|
|
|
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
|
final List<dynamic> data = response.data;
|
|
final stickers = data.map((e) => SnStickerPack.fromJson(e)).toList();
|
|
|
|
final hasMore = offset + stickers.length < total;
|
|
final nextCursor = hasMore ? (offset + stickers.length).toString() : null;
|
|
|
|
return CursorPagingData(
|
|
items: stickers,
|
|
hasMore: hasMore,
|
|
nextCursor: nextCursor,
|
|
);
|
|
} catch (err) {
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
@riverpod
|
|
Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
|
|
if (packId == null) return null;
|
|
final apiClient = ref.watch(apiClientProvider);
|
|
final resp = await apiClient.get('/stickers/$packId');
|
|
return SnStickerPack.fromJson(resp.data);
|
|
}
|
|
|
|
@RoutePage()
|
|
class NewStickerPacksScreen extends HookConsumerWidget {
|
|
final String pubName;
|
|
const NewStickerPacksScreen({
|
|
super.key,
|
|
@PathParam("name") required this.pubName,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return EditStickerPacksScreen(pubName: pubName);
|
|
}
|
|
}
|
|
|
|
@RoutePage()
|
|
class EditStickerPacksScreen extends HookConsumerWidget {
|
|
final String pubName;
|
|
final String? packId;
|
|
const EditStickerPacksScreen({
|
|
super.key,
|
|
@PathParam("name") required this.pubName,
|
|
@PathParam("packId") this.packId,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
|
final initialPack = ref.watch(stickerPackProvider(packId));
|
|
|
|
final nameController = useTextEditingController();
|
|
final descriptionController = useTextEditingController();
|
|
final prefixController = useTextEditingController();
|
|
|
|
useEffect(() {
|
|
if (initialPack.value != null) {
|
|
nameController.text = initialPack.value!.name;
|
|
descriptionController.text = initialPack.value!.description;
|
|
prefixController.text = initialPack.value!.prefix;
|
|
}
|
|
return null;
|
|
}, [initialPack]);
|
|
|
|
final submitting = useState(false);
|
|
|
|
Future<void> submit() async {
|
|
if (!(formKey.currentState?.validate() ?? false)) return;
|
|
|
|
try {
|
|
submitting.value = true;
|
|
final apiClient = ref.watch(apiClientProvider);
|
|
final resp = await apiClient.request(
|
|
'/stickers',
|
|
data: {
|
|
'name': nameController.text,
|
|
'description': descriptionController.text,
|
|
'prefix': prefixController.text,
|
|
},
|
|
options: Options(
|
|
method: packId == null ? 'POST' : 'PATCH',
|
|
headers: {'X-Pub': pubName},
|
|
),
|
|
);
|
|
if (!context.mounted) return;
|
|
context.router.maybePop(SnStickerPack.fromJson(resp.data));
|
|
} catch (err) {
|
|
showErrorAlert(err);
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
|
|
return AppScaffold(
|
|
appBar: AppBar(
|
|
title:
|
|
Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(),
|
|
),
|
|
body: Column(
|
|
children: [
|
|
Form(
|
|
key: formKey,
|
|
child: Column(
|
|
spacing: 8,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
TextFormField(
|
|
controller: nameController,
|
|
decoration: InputDecoration(
|
|
labelText: 'name'.tr(),
|
|
border: const UnderlineInputBorder(),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'fieldCannotBeEmpty'.tr();
|
|
}
|
|
return null;
|
|
},
|
|
onTapOutside:
|
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
),
|
|
TextFormField(
|
|
controller: descriptionController,
|
|
decoration: InputDecoration(
|
|
labelText: 'description'.tr(),
|
|
border: const UnderlineInputBorder(),
|
|
),
|
|
minLines: 3,
|
|
maxLines: null,
|
|
onTapOutside:
|
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
),
|
|
TextFormField(
|
|
controller: prefixController,
|
|
decoration: InputDecoration(
|
|
labelText: 'stickerPackPrefix'.tr(),
|
|
border: const UnderlineInputBorder(),
|
|
helperText: 'deleteStickerHint'.tr(),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'fieldCannotBeEmpty'.tr();
|
|
}
|
|
return null;
|
|
},
|
|
onTapOutside:
|
|
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Gap(12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: TextButton.icon(
|
|
onPressed: submitting.value ? null : submit,
|
|
icon: const Icon(Symbols.save),
|
|
label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()),
|
|
),
|
|
),
|
|
],
|
|
).padding(horizontal: 24, vertical: 16),
|
|
);
|
|
}
|
|
}
|