diff --git a/lib/screens/album.dart b/lib/screens/album.dart index 4c91e30..f27adb0 100644 --- a/lib/screens/album.dart +++ b/lib/screens/album.dart @@ -1,10 +1,125 @@ +import 'package:dismissible_page/dismissible_page.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/attachment.dart'; +import 'package:surface/widgets/attachment/attachment_detail.dart'; +import 'package:surface/widgets/attachment/attachment_item.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:uuid/uuid.dart'; -class AlbumScreen extends StatelessWidget { +class AlbumScreen extends StatefulWidget { const AlbumScreen({super.key}); + @override + State createState() => _AlbumScreenState(); +} + +class _AlbumScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + bool _isBusy = false; + int? _totalCount; + + final List _attachments = List.empty(growable: true); + final List _heroTags = List.empty(growable: true); + + Future _fetchAttachments() async { + setState(() => _isBusy = true); + + const uuid = Uuid(); + + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { + 'take': 10, + 'offset': _attachments.length, + }); + final attachments = List.from( + resp.data['data']?.map((e) => SnAttachment.fromJson(e)) ?? [], + ).where((e) => e.mimetype.startsWith('image')).toList(); + _attachments.addAll(attachments); + _heroTags.addAll(_attachments.map((_) => uuid.v4())); + + _totalCount = resp.data['count'] as int?; + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchAttachments(); + _scrollController.addListener(() { + if (_scrollController.position.atEdge) { + bool isTop = _scrollController.position.pixels == 0; + if (!isTop && !_isBusy) { + if (_totalCount == null || _attachments.length < _totalCount!) { + _fetchAttachments(); + } + } + } + }); + } + + @override + void dispose() { + super.dispose(); + _scrollController.dispose(); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + body: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + title: Text('screenAlbum').tr(), + ), + SliverMasonryGrid.extent( + childCount: _attachments.length, + maxCrossAxisExtent: 320, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + itemBuilder: (context, idx) { + final attachment = _attachments[idx]; + return GestureDetector( + child: ClipRRect( + child: AspectRatio( + aspectRatio: attachment.metadata['ratio']?.toDouble() ?? 1, + child: AttachmentItem( + data: attachment, + heroTag: _heroTags[idx], + ), + ), + ), + onTap: () { + context.pushTransparentRoute( + AttachmentZoomView( + data: [attachment], + heroTags: [_heroTags[idx]], + ), + backgroundColor: Colors.black.withOpacity(0.7), + rootNavigator: true, + ); + }, + ); + }, + ), + if (_isBusy) + SliverToBoxAdapter( + child: const CircularProgressIndicator().padding(all: 24), + ), + ], + ), + ); } } diff --git a/lib/screens/chat/channel_detail.dart b/lib/screens/chat/channel_detail.dart index bed7a0a..78fa82c 100644 --- a/lib/screens/chat/channel_detail.dart +++ b/lib/screens/chat/channel_detail.dart @@ -159,6 +159,8 @@ class _ChannelDetailScreenState extends State { .fontSize(17) .tr() .padding(horizontal: 20, bottom: 4), + // TODO add notify level modifier + // TODO impl this ListTile( leading: AccountImage( content: diff --git a/lib/widgets/attachment/attachment_item.dart b/lib/widgets/attachment/attachment_item.dart index ef7cd54..5c7657d 100644 --- a/lib/widgets/attachment/attachment_item.dart +++ b/lib/widgets/attachment/attachment_item.dart @@ -95,7 +95,7 @@ class _AttachmentItemSensitiveBlurState color: Colors.black.withOpacity(0.5), alignment: Alignment.center, child: Container( - constraints: const BoxConstraints(maxWidth: 280), + constraints: const BoxConstraints(maxWidth: 180), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,12 +105,15 @@ class _AttachmentItemSensitiveBlurState size: 32, ), const Gap(8), - Text('sensitiveContent') + Text('sensitiveContent', textAlign: TextAlign.center) .tr() .fontSize(20) .textColor(Colors.white) .bold(), - Text('sensitiveContentDescription') + Text( + 'sensitiveContentDescription', + textAlign: TextAlign.center, + ) .tr() .fontSize(14) .textColor(Colors.white.withOpacity(0.8)), diff --git a/pubspec.yaml b/pubspec.yaml index 8ebd366..16a116a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.0.0+10 +version: 2.0.0+11 environment: sdk: ^3.5.4