Surface/lib/screens/stickers.dart

347 lines
10 KiB
Dart

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController =
TabController(length: 3, vsync: this);
bool _isBusy = false;
int? _totalCount;
final List<SnStickerPack> _packs = List.empty(growable: true);
Future<void> _fetchPacks() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>();
final resp = await sn.client.get(
_tabController.index == 1
? '/cgi/uc/stickers/packs/own'
: '/cgi/uc/stickers/packs',
queryParameters: {
'take': 10,
'offset': _packs.length,
if (_tabController.index == 2) 'author': ua.user?.id,
},
);
if (resp.data is Map<String, dynamic>) {
_totalCount = resp.data['count'] as int?;
final out = List<SnStickerPack>.from(
resp.data['data'].map((ele) => SnStickerPack.fromJson(ele)),
);
_packs.addAll(out);
} else {
_totalCount = 0;
final out = List<SnStickerPack>.from(
resp.data.map((ele) => SnStickerPack.fromJson(ele)),
);
_packs.addAll(out);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _removePack(SnStickerPack pack) async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}/own');
if (!mounted) return;
context.showSnackbar('stickersRemoved'.tr());
_refreshPacks();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _deletePack(SnStickerPack pack) async {
final confirm = await context.showConfirmDialog(
'stickersPackDelete'.tr(args: [pack.name]),
'stickersPackDeleteDescription'.tr(),
);
if (!confirm) return;
if (!mounted) return;
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/uc/stickers/packs/${pack.id}');
if (!mounted) return;
context.showSnackbar('stickersDeleted'.tr());
_refreshPacks();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _refreshPacks() async {
_packs.clear();
_totalCount = null;
await _fetchPacks();
}
@override
void initState() {
super.initState();
_fetchPacks();
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
_refreshPacks();
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
leading: AutoAppBarLeading(),
title: Text('screenStickers').tr(),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(
child: Text('stickersDiscovery'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
Tab(
child: Text('stickersOwned'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
Tab(
child: Text('stickersCreated'.tr()).textColor(
Theme.of(context).appBarTheme.foregroundColor,
),
),
],
),
),
body: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _refreshPacks,
child: InfiniteList(
itemCount: _packs.length,
onFetchData: _fetchPacks,
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
isLoading: _isBusy,
itemBuilder: (context, idx) {
final pack = _packs[idx];
return ListTile(
title: Text(pack.name),
subtitle: Text(
pack.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
trailing: _tabController.index == 1
? IconButton(
onPressed: () {
_removePack(pack);
},
icon: const Icon(Symbols.remove),
)
: _tabController.index == 2
? IconButton(
onPressed: () {
_deletePack(pack);
},
icon: const Icon(Symbols.delete),
)
: null,
onTap: () {
if (_tabController.index == 0) {
showModalBottomSheet(
context: context,
builder: (context) => _StickerPackAddPopup(pack: pack),
).then((value) {
if (value == true && _tabController.index == 1) {
_refreshPacks();
}
});
} else {
GoRouter.of(context).pushNamed(
'stickerPack',
pathParameters: {
'id': pack.id.toString(),
},
);
}
},
);
},
),
),
),
);
}
}
class _StickerPackAddPopup extends StatefulWidget {
final SnStickerPack pack;
const _StickerPackAddPopup({required this.pack});
@override
State<_StickerPackAddPopup> createState() => _StickerPackAddPopupState();
}
class _StickerPackAddPopupState extends State<_StickerPackAddPopup> {
SnStickerPack? _pack;
bool _isBusy = false;
Future<void> _fetchPack() async {
try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/uc/stickers/packs/${widget.pack.id}');
_pack = SnStickerPack.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPack();
}
bool _isAdding = false;
Future<void> _addPack() async {
if (_pack == null) return;
try {
setState(() => _isAdding = true);
final sn = context.read<SnNetworkProvider>();
final stickers = context.read<SnStickerProvider>();
await sn.client.post(
'/cgi/uc/stickers/packs/${widget.pack.id}/own',
);
if (!mounted) return;
context.showSnackbar('stickersAdded'.tr());
if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!);
Navigator.pop(context, true);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isAdding = false);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.add, size: 24),
const Gap(16),
Text('stickersAdd', style: Theme.of(context).textTheme.titleLarge)
.tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.pack.name).bold(),
Text(
widget.pack.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
ElevatedButton(
onPressed: _isAdding ? null : _addPack,
child: Text('add').tr(),
),
],
).padding(horizontal: 24),
LoadingIndicator(isActive: _isBusy),
if (_pack?.stickers != null)
Expanded(
child: GridView.extent(
padding: EdgeInsets.only(left: 20, right: 20, top: 8),
maxCrossAxisExtent: 48,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: _pack!.stickers!
.map(
(ele) => ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: AttachmentItem(
data: ele.attachment,
heroTag: 'sticker-pack-${ele.attachment.rid}',
fit: BoxFit.contain,
),
),
),
)
.toList(),
),
),
],
);
}
}