From 2b61c372f53d338efae7ca347f7e02e9d37efc32 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 3 Mar 2025 20:53:42 +0800 Subject: [PATCH] :sparkles: Allow profile picture (avatar & banner) upload gif --- lib/screens/account/profile_edit.dart | 182 +++++++++--------- .../account/publishers/publisher_edit.dart | 53 ++--- lib/screens/notification.dart | 106 +++------- 3 files changed, 143 insertions(+), 198 deletions(-) diff --git a/lib/screens/account/profile_edit.dart b/lib/screens/account/profile_edit.dart index 23e6f7c..a676dc2 100644 --- a/lib/screens/account/profile_edit.dart +++ b/lib/screens/account/profile_edit.dart @@ -68,38 +68,35 @@ class _ProfileEditScreenState extends State { _banner = prof.banner; _links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); _birthday = prof.profile!.birthday?.toLocal(); - if(_birthday != null) { - _birthdayController.text = DateFormat(_kDateFormat).format( - prof.profile!.birthday!.toLocal(), - ); + if (_birthday != null) { + _birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); } } void _selectBirthday() async { await showCupertinoModalPopup( context: context, - builder: (BuildContext context) => Container( - height: 216, - padding: const EdgeInsets.only(top: 6.0), - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - color: Theme.of(context).colorScheme.surface, - child: SafeArea( - top: false, - child: CupertinoDatePicker( - initialDateTime: _birthday?.toLocal(), - mode: CupertinoDatePickerMode.date, - use24hFormat: true, - onDateTimeChanged: (DateTime newDate) { - setState(() { - _birthday = newDate; - _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); - }); - }, + builder: + (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + color: Theme.of(context).colorScheme.surface, + child: SafeArea( + top: false, + child: CupertinoDatePicker( + initialDateTime: _birthday?.toLocal(), + mode: CupertinoDatePickerMode.date, + use24hFormat: true, + onDateTimeChanged: (DateTime newDate) { + setState(() { + _birthday = newDate; + _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); + }); + }, + ), + ), ), - ), - ), ); } @@ -108,32 +105,41 @@ class _ProfileEditScreenState extends State { if (image == null) return; if (!mounted) return; - final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); - final aspectRatios = - place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; - final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) - ? await showCupertinoImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ) - : await showMaterialImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ); + final skipCrop = image.path.endsWith('.gif'); - if (result == null) return; + Uint8List? rawBytes; + if (!skipCrop) { + final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); + final aspectRatios = + place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; + final result = + (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) + ? await showCupertinoImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ) + : await showMaterialImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ); + + if (result == null) return; + + if (!mounted) return; + setState(() => _isBusy = true); + rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); + } else { + if (!mounted) return; + setState(() => _isBusy = true); + rawBytes = await image.readAsBytes(); + } - if (!mounted) return; final attach = context.read(); - setState(() => _isBusy = true); - - final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); - try { final attachment = await attach.directUploadOne( rawBytes, @@ -145,10 +151,7 @@ class _ProfileEditScreenState extends State { if (!mounted) return; final sn = context.read(); - await sn.client.put( - '/cgi/id/users/me/$place', - data: {'attachment': attachment.rid}, - ); + await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); if (!mounted) return; final ua = context.read(); @@ -184,7 +187,7 @@ class _ProfileEditScreenState extends State { 'location': _locationController.value.text, 'birthday': _birthday?.toUtc().toIso8601String(), 'links': { - for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2 + for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2, }, }, ); @@ -231,10 +234,7 @@ class _ProfileEditScreenState extends State { final sn = context.read(); return AppScaffold( - appBar: AppBar( - leading: const PageBackButton(), - title: Text('screenAccountProfileEdit').tr(), - ), + appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()), body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -253,12 +253,10 @@ class _ProfileEditScreenState extends State { aspectRatio: 16 / 9, child: Container( color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: _banner != null - ? AutoResizeUniversalImage( - sn.getAttachmentUrl(_banner!), - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + child: + _banner != null + ? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) + : const SizedBox.shrink(), ), ), ), @@ -299,10 +297,7 @@ class _ProfileEditScreenState extends State { ), TextField( controller: _nicknameController, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldNickname'.tr(), - ), + decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), Row( @@ -364,10 +359,7 @@ class _ProfileEditScreenState extends State { keyboardType: TextInputType.multiline, maxLines: null, minLines: 3, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldDescription'.tr(), - ), + decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), Row( @@ -384,42 +376,40 @@ class _ProfileEditScreenState extends State { ), ), const Gap(4), - StyledWidget(IconButton( - icon: const Icon(Symbols.calendar_month), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - _timezoneController.text = await FlutterTimezone.getLocalTimezone(); - }, - )).padding(top: 6), + StyledWidget( + IconButton( + icon: const Icon(Symbols.calendar_month), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () async { + _timezoneController.text = await FlutterTimezone.getLocalTimezone(); + }, + ), + ).padding(top: 6), const Gap(4), - StyledWidget(IconButton( - icon: const Icon(Symbols.clear), - visualDensity: VisualDensity(horizontal: -4, vertical: -4), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () { - _timezoneController.clear(); - }, - )).padding(top: 6), + StyledWidget( + IconButton( + icon: const Icon(Symbols.clear), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + _timezoneController.clear(); + }, + ), + ).padding(top: 6), ], ), TextField( controller: _locationController, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldLocation'.tr(), - ), + decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), TextField( controller: _birthdayController, readOnly: true, - decoration: InputDecoration( - border: const UnderlineInputBorder(), - labelText: 'fieldBirthday'.tr(), - ), + decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()), onTap: () => _selectBirthday(), ), if (_links != null) diff --git a/lib/screens/account/publishers/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart index 147ec8e..0e9f4d1 100644 --- a/lib/screens/account/publishers/publisher_edit.dart +++ b/lib/screens/account/publishers/publisher_edit.dart @@ -108,32 +108,41 @@ class _AccountPublisherEditScreenState extends State if (image == null) return; if (!mounted) return; - final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); - final aspectRatios = - place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; - final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) - ? await showCupertinoImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ) - : await showMaterialImageCropper( - // ignore: use_build_context_synchronously - context, - allowedAspectRatios: aspectRatios, - imageProvider: imageProvider, - ); + final skipCrop = image.path.endsWith('.gif'); - if (result == null) return; + Uint8List? rawBytes; + if (!skipCrop) { + final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); + final aspectRatios = + place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; + final result = + (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) + ? await showCupertinoImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ) + : await showMaterialImageCropper( + // ignore: use_build_context_synchronously + context, + allowedAspectRatios: aspectRatios, + imageProvider: imageProvider, + ); + + if (result == null) return; + + if (!mounted) return; + setState(() => _isBusy = true); + rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); + } else { + if (!mounted) return; + setState(() => _isBusy = true); + rawBytes = await image.readAsBytes(); + } - if (!mounted) return; final attach = context.read(); - setState(() => _isBusy = true); - - final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); - try { final attachment = await attach.directUploadOne( rawBytes, diff --git a/lib/screens/notification.dart b/lib/screens/notification.dart index 8cb2127..4587fab 100644 --- a/lib/screens/notification.dart +++ b/lib/screens/notification.dart @@ -58,18 +58,12 @@ class _NotificationScreenState extends State { try { final sn = context.read(); final nty = context.read(); - final resp = - await sn.client.get('/cgi/id/notifications', queryParameters: { - 'take': 10, - 'offset': _notifications.length, - }); - _totalCount = resp.data['count']; - _notifications.addAll( - resp.data['data'] - ?.map((e) => SnNotification.fromJson(e)) - .cast() ?? - [], + final resp = await sn.client.get( + '/cgi/id/notifications', + queryParameters: {'take': 10, 'offset': _notifications.length}, ); + _totalCount = resp.data['count']; + _notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast() ?? []); nty.updateTray(); } catch (err) { if (!mounted) return; @@ -104,9 +98,7 @@ class _NotificationScreenState extends State { nty.clear(); if (!mounted) return; - context.showSnackbar( - 'notificationMarkAllReadPrompt'.plural(resp.data['count']), - ); + context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count'])); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -130,9 +122,7 @@ class _NotificationScreenState extends State { _fetchNotifications(); if (!mounted) return; - context.showSnackbar( - 'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']), - ); + context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}'])); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -153,13 +143,8 @@ class _NotificationScreenState extends State { if (!ua.isAuthorized) { return AppScaffold( - appBar: AppBar( - leading: AutoAppBarLeading(), - title: Text('screenNotification').tr(), - ), - body: Center( - child: UnauthorizedHint(), - ), + appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()), + body: Center(child: UnauthorizedHint()), ); } @@ -168,10 +153,7 @@ class _NotificationScreenState extends State { leading: AutoAppBarLeading(), title: Text('screenNotification').tr(), actions: [ - IconButton( - icon: const Icon(Symbols.checklist), - onPressed: _isSubmitting ? null : _markAllAsRead, - ), + IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead), const Gap(8), ], ), @@ -185,17 +167,13 @@ class _NotificationScreenState extends State { return _fetchNotifications(); }, child: InfiniteList( - padding: EdgeInsets.only( - top: 16, - bottom: math.max(MediaQuery.of(context).padding.bottom, 16), - ), + padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)), itemCount: _notifications.length, onFetchData: () { _fetchNotifications(); }, isLoading: _isBusy, - hasReachedMax: _totalCount != null && - _notifications.length >= _totalCount!, + hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!, itemBuilder: (context, idx) { final nty = _notifications[idx]; return Row( @@ -208,45 +186,26 @@ class _NotificationScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (nty.readAt == null) - StyledWidget(Badge( - label: Text('notificationUnread').tr(), - )).padding(bottom: 4), - Text( - nty.title, - style: Theme.of(context).textTheme.titleMedium, - ), + StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4), + Text(nty.title, style: Theme.of(context).textTheme.titleMedium), if (nty.subtitle != null) - Text( - nty.subtitle!, - style: Theme.of(context).textTheme.titleSmall, - ), + Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall), if (nty.subtitle != null) const Gap(4), - SelectionArea( - child: MarkdownTextContent( - content: nty.body, - isAutoWarp: true, - ), - ), + SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)), if ([ 'interactive.reply', 'interactive.feedback', - 'interactive.subscription' + 'interactive.subscription', ].contains(nty.topic) && nty.metadata['related_post'] != null) GestureDetector( child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8)), - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1, - ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + border: Border.all(color: Theme.of(context).dividerColor, width: 1), ), child: PostItem( - data: SnPost.fromJson( - nty.metadata['related_post']!, - ), + data: SnPost.fromJson(nty.metadata['related_post']!), showComments: false, showReactions: false, showMenu: false, @@ -255,29 +214,18 @@ class _NotificationScreenState extends State { onTap: () { GoRouter.of(context).pushNamed( 'postDetail', - pathParameters: { - 'slug': nty - .metadata['related_post']!['id'] - .toString(), - }, + pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()}, ); }, ).padding(top: 8), const Gap(8), Row( children: [ - Text( - DateFormat('yy/MM/dd').format(nty.createdAt), - ).fontSize(12), + Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12), const Gap(4), - Text( - '·', - style: TextStyle(fontSize: 12), - ), + Text('·', style: TextStyle(fontSize: 12)), const Gap(4), - Text( - RelativeTime(context).format(nty.createdAt), - ).fontSize(12), + Text(RelativeTime(context).format(nty.createdAt)).fontSize(12), ], ).opacity(0.75), ], @@ -287,10 +235,8 @@ class _NotificationScreenState extends State { IconButton( icon: const Icon(Symbols.check), padding: EdgeInsets.all(0), - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), - onPressed: - _isSubmitting ? null : () => _markOneAsRead(nty), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), + onPressed: _isSubmitting ? null : () => _markOneAsRead(nty), ), ], ).padding(horizontal: 16);