Editable post

This commit is contained in:
LittleSheep 2024-11-10 18:37:34 +08:00
parent 60a34fed00
commit 3ffe3cb50f
18 changed files with 249 additions and 93 deletions

View File

@ -31,6 +31,7 @@
"preview": "Preview", "preview": "Preview",
"loading": "Loading...", "loading": "Loading...",
"delete": "Delete", "delete": "Delete",
"report": "Report",
"fieldUsername": "Username", "fieldUsername": "Username",
"fieldNickname": "Nickname", "fieldNickname": "Nickname",
"fieldEmail": "Email address", "fieldEmail": "Email address",
@ -72,5 +73,6 @@
"fieldPostContent": "What happened?!", "fieldPostContent": "What happened?!",
"fieldPostTitle": "Title", "fieldPostTitle": "Title",
"fieldPostDescription": "Description", "fieldPostDescription": "Description",
"postPublish": "Publish" "postPublish": "Publish",
"postEditingNotice": "You're about to editing a post that posted {}."
} }

View File

@ -31,6 +31,7 @@
"create": "创建", "create": "创建",
"preview": "预览", "preview": "预览",
"delete": "删除", "delete": "删除",
"report": "检举",
"fieldUsername": "用户名", "fieldUsername": "用户名",
"fieldNickname": "显示名", "fieldNickname": "显示名",
"fieldEmail": "电子邮箱地址", "fieldEmail": "电子邮箱地址",
@ -72,5 +73,6 @@
"fieldPostContent": "发生什么事了?!", "fieldPostContent": "发生什么事了?!",
"fieldPostTitle": "标题", "fieldPostTitle": "标题",
"fieldPostDescription": "描述", "fieldPostDescription": "描述",
"postPublish": "发布" "postPublish": "发布",
"postEditingNotice": "你正在修改由 {} 发布的帖子。"
} }

View File

@ -46,6 +46,15 @@ final appRouter = GoRouter(
name: 'postEditor', name: 'postEditor',
builder: (context, state) => PostEditorScreen( builder: (context, state) => PostEditorScreen(
mode: state.pathParameters['mode']!, mode: state.pathParameters['mode']!,
postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
), ),
), ),
], ],

View File

@ -79,7 +79,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
subtitle: Text('accountProfileEditSubtitle').tr(), subtitle: Text('accountProfileEditSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.contact_page), leading: const Icon(Symbols.contact_page),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountProfileEdit'); GoRouter.of(context).pushNamed('accountProfileEdit');
}, },
@ -89,7 +89,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
subtitle: Text('accountPublishersSubtitle').tr(), subtitle: Text('accountPublishersSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.face), leading: const Icon(Symbols.face),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountPublishers'); GoRouter.of(context).pushNamed('accountPublishers');
}, },
@ -99,7 +99,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
subtitle: Text('accountLogoutSubtitle').tr(), subtitle: Text('accountLogoutSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.logout), leading: const Icon(Symbols.logout),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
context context
.showConfirmDialog( .showConfirmDialog(
@ -147,7 +147,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
subtitle: Text('screenAuthLoginSubtitle').tr(), subtitle: Text('screenAuthLoginSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.login), leading: const Icon(Symbols.login),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('authLogin'); GoRouter.of(context).pushNamed('authLogin');
}, },
@ -157,7 +157,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
subtitle: Text('screenAuthRegisterSubtitle').tr(), subtitle: Text('screenAuthRegisterSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.person_add), leading: const Icon(Symbols.person_add),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('authRegister'); GoRouter.of(context).pushNamed('authRegister');
}, },

View File

@ -121,7 +121,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _isBusy ? null : _performAction, onPressed: _isBusy ? null : _performAction,
icon: const Icon(Icons.add), icon: const Icon(Symbols.add),
label: Text('create').tr(), label: Text('create').tr(),
), ),
).padding(horizontal: 2), ).padding(horizontal: 2),

View File

@ -96,7 +96,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
const Icon(Icons.edit), const Icon(Symbols.edit),
const Gap(16), const Gap(16),
Text('edit').tr(), Text('edit').tr(),
], ],

View File

@ -12,8 +12,8 @@ import 'package:surface/widgets/dialog.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = { final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
0: ('authFactorPassword'.tr(), Icons.password, false), 0: ('authFactorPassword'.tr(), Symbols.password, false),
1: ('authFactorEmail'.tr(), Icons.email, true), 1: ('authFactorEmail'.tr(), Symbols.email, true),
}; };
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
@ -225,7 +225,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('next').tr(), Text('next').tr(),
const Icon(Icons.chevron_right), const Icon(Symbols.chevron_right),
], ],
), ),
), ),
@ -320,7 +320,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
), ),
), ),
secondary: Icon( secondary: Icon(
_factorLabelMap[x.type]?.$2 ?? Icons.question_mark, _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
), ),
title: Text( title: Text(
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
@ -355,7 +355,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('next'.tr()), Text('next'.tr()),
const Icon(Icons.chevron_right), const Icon(Symbols.chevron_right),
], ],
), ),
), ),
@ -505,7 +505,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('next').tr(), Text('next').tr(),
const Icon(Icons.chevron_right), const Icon(Symbols.chevron_right),
], ],
), ),
), ),
@ -538,7 +538,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
children: [ children: [
Text('termAcceptLink'.tr()), Text('termAcceptLink'.tr()),
const Gap(4), const Gap(4),
const Icon(Icons.launch, size: 14), const Icon(Symbols.launch, size: 14),
], ],
), ),
onTap: () { onTap: () {

View File

@ -146,7 +146,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text('next').tr(), Text('next').tr(),
const Icon(Icons.chevron_right), const Icon(Symbols.chevron_right),
], ],
), ),
), ),

View File

@ -27,7 +27,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
final List<SnPost> _posts = List.empty(growable: true); final List<SnPost> _posts = List.empty(growable: true);
int? _postCount; int? _postCount;
void _fetchPosts() async { Future<void> _fetchPosts() async {
if (_postCount != null && _posts.length >= _postCount!) return; if (_postCount != null && _posts.length >= _postCount!) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -75,9 +75,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar(
title: Text('screenExplore').tr(),
),
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -155,15 +152,31 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
], ],
), ),
body: InfiniteList( body: RefreshIndicator(
itemCount: _posts.length, displacement: 40 + MediaQuery.of(context).padding.top,
isLoading: _isBusy, onRefresh: () {
hasReachedMax: _postCount != null && _posts.length >= _postCount!, _posts.clear();
onFetchData: _fetchPosts, return _fetchPosts();
itemBuilder: (context, idx) {
return PostItem(data: _posts[idx]);
}, },
separatorBuilder: (context, index) => const Divider(), child: CustomScrollView(
slivers: [
SliverAppBar(
title: Text('screenExplore').tr(),
floating: true,
snap: true,
),
SliverInfiniteList(
itemCount: _posts.length,
isLoading: _isBusy,
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
return PostItem(data: _posts[idx]);
},
separatorBuilder: (context, index) => const Divider(),
)
],
),
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -12,7 +13,9 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -20,7 +23,16 @@ import 'package:provider/provider.dart';
class PostEditorScreen extends StatefulWidget { class PostEditorScreen extends StatefulWidget {
final String mode; final String mode;
const PostEditorScreen({super.key, required this.mode}); final int? postEditId;
final int? postReplyId;
final int? postRepostId;
const PostEditorScreen({
super.key,
required this.mode,
required this.postEditId,
required this.postReplyId,
required this.postRepostId,
});
@override @override
State<PostEditorScreen> createState() => _PostEditorScreenState(); State<PostEditorScreen> createState() => _PostEditorScreenState();
@ -33,6 +45,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}; };
bool _isBusy = false; bool _isBusy = false;
bool _isLoading = false;
SnPublisher? _publisher; SnPublisher? _publisher;
List<SnPublisher>? _publishers; List<SnPublisher>? _publishers;
@ -40,7 +53,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final List<XFile> _selectedMedia = List.empty(growable: true); final List<XFile> _selectedMedia = List.empty(growable: true);
final List<SnAttachment> _attachments = List.empty(growable: true); final List<SnAttachment> _attachments = List.empty(growable: true);
void _fetchPublishers() async { Future<void> _fetchPublishers() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers'); final resp = await sn.client.get('/cgi/co/publishers');
_publishers = List<SnPublisher>.from( _publishers = List<SnPublisher>.from(
@ -51,6 +64,63 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}); });
} }
SnPost? _editingOg;
SnPost? _replyingTo;
SnPost? _repostingTo;
Future<void> _fetchRelatedPost() async {
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
try {
setState(() => _isLoading = true);
if (widget.postEditId != null) {
final resp = await sn.client.get('/cgi/co/posts/${widget.postEditId}');
final post = SnPost.fromJson(resp.data);
final attachments = await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
_title = post.body['title'];
_description = post.body['description'];
_contentController.text = post.body['content'] ?? '';
_attachments.addAll(attachments);
_editingOg = post.copyWith(
preload: SnPostPreload(
attachments: attachments,
),
);
}
if (widget.postReplyId != null) {
final resp = await sn.client.get('/cgi/co/posts/${widget.postReplyId}');
final post = SnPost.fromJson(resp.data);
_replyingTo = post.copyWith(
preload: SnPostPreload(
attachments: await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
),
);
}
if (widget.postRepostId != null) {
final resp =
await sn.client.get('/cgi/co/posts/${widget.postRepostId}');
final post = SnPost.fromJson(resp.data);
_repostingTo = post.copyWith(
preload: SnPostPreload(
attachments: await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
),
);
}
} catch (err) {
context.showErrorDialog(err);
} finally {
setState(() => _isLoading = false);
}
}
String? _title; String? _title;
String? _description; String? _description;
@ -110,24 +180,35 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
// Posting the content // Posting the content
try { try {
final baseProgressVal = _progress!; final baseProgressVal = _progress!;
await sn.client.post('/cgi/co/${widget.mode}', data: { await sn.client.request(
'publisher': _publisher!.id, [
'content': _contentController.value.text, '/cgi/co/${widget.mode}',
'title': _title, if (widget.postEditId != null) '${widget.postEditId}',
'description': _description, ].join('/'),
'attachments': _attachments.map((e) => e.rid).toList(), data: {
}, onSendProgress: (count, total) { 'publisher': _publisher!.id,
setState(() { 'content': _contentController.value.text,
_progress = 'title': _title,
baseProgressVal + (count / total) * (kPostingProgressWeight / 2); 'description': _description,
}); 'attachments': _attachments.map((e) => e.rid).toList(),
}, onReceiveProgress: (count, total) { },
setState(() { onSendProgress: (count, total) {
_progress = baseProgressVal + setState(() {
(kPostingProgressWeight / 2) + _progress = baseProgressVal +
(count / total) * (kPostingProgressWeight / 2); (count / total) * (kPostingProgressWeight / 2);
}); });
}); },
onReceiveProgress: (count, total) {
setState(() {
_progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
});
},
options: Options(
method: widget.postEditId != null ? 'PUT' : 'POST',
),
);
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (err) { } catch (err) {
@ -177,6 +258,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
context.showErrorDialog('Unknown post type'); context.showErrorDialog('Unknown post type');
Navigator.pop(context); Navigator.pop(context);
} }
_fetchRelatedPost();
_fetchPublishers(); _fetchPublishers();
} }
@ -221,6 +303,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
items: <DropdownMenuItem<SnPublisher>>[ items: <DropdownMenuItem<SnPublisher>>[
...(_publishers?.map( ...(_publishers?.map(
(item) => DropdownMenuItem<SnPublisher>( (item) => DropdownMenuItem<SnPublisher>(
enabled: _editingOg == null,
value: item, value: item,
child: Row( child: Row(
children: [ children: [
@ -302,9 +385,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8), padding: EdgeInsets.only(
top: _editingOg == null ? 8 : 0,
bottom: 8,
),
child: Column( child: Column(
children: [ children: [
// Editing Notice
if (_editingOg != null)
Column(
children: [
Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
minTileHeight: 48,
leading:
const Icon(Symbols.edit_note).padding(left: 4),
title: Text('postEditingNotice')
.fontSize(15)
.tr(args: ['@${_editingOg!.publisher.name}']),
children: <Widget>[
PostItem(data: _editingOg!),
],
),
),
const Divider(height: 1),
const Gap(8)
],
),
// Content Input Area
TextField( TextField(
controller: _contentController, controller: _contentController,
maxLines: null, maxLines: null,
@ -338,6 +448,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LoadingIndicator(isActive: _isBusy),
if (_isBusy && _progress != null) if (_isBusy && _progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1), tween: Tween(begin: 0, end: 1),

View File

@ -6,6 +6,8 @@ part 'post.g.dart';
@freezed @freezed
class SnPost with _$SnPost { class SnPost with _$SnPost {
const SnPost._();
const factory SnPost({ const factory SnPost({
required int id, required int id,
required DateTime createdAt, required DateTime createdAt,
@ -44,6 +46,11 @@ class SnPost with _$SnPost {
}) = _SnPost; }) = _SnPost;
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json); factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
String get typePlural => switch (type) {
'story' => 'stories',
_ => '${type}s',
};
} }
@freezed @freezed

View File

@ -577,7 +577,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$SnPostImpl implements _SnPost { class _$SnPostImpl extends _SnPost {
const _$SnPostImpl( const _$SnPostImpl(
{required this.id, {required this.id,
required this.createdAt, required this.createdAt,
@ -615,7 +615,8 @@ class _$SnPostImpl implements _SnPost {
this.preload}) this.preload})
: _body = body, : _body = body,
_tags = tags, _tags = tags,
_categories = categories; _categories = categories,
super._();
factory _$SnPostImpl.fromJson(Map<String, dynamic> json) => factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
_$$SnPostImplFromJson(json); _$$SnPostImplFromJson(json);
@ -827,7 +828,7 @@ class _$SnPostImpl implements _SnPost {
} }
} }
abstract class _SnPost implements SnPost { abstract class _SnPost extends SnPost {
const factory _SnPost( const factory _SnPost(
{required final int id, {required final int id,
required final DateTime createdAt, required final DateTime createdAt,
@ -863,6 +864,7 @@ abstract class _SnPost implements SnPost {
required final SnPublisher publisher, required final SnPublisher publisher,
required final SnMetric metric, required final SnMetric metric,
final SnPostPreload? preload}) = _$SnPostImpl; final SnPostPreload? preload}) = _$SnPostImpl;
const _SnPost._() : super._();
factory _SnPost.fromJson(Map<String, dynamic> json) = _$SnPostImpl.fromJson; factory _SnPost.fromJson(Map<String, dynamic> json) = _$SnPostImpl.fromJson;

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -39,7 +40,7 @@ class AccountImage extends StatelessWidget {
child: (content?.isEmpty ?? true) child: (content?.isEmpty ?? true)
? (fallbackWidget ?? ? (fallbackWidget ??
Icon( Icon(
Icons.account_circle, Symbols.account_circle,
size: radius != null ? radius! * 1.2 : 24, size: radius != null ? radius! * 1.2 : 24,
color: foregroundColor, color: foregroundColor,
)) ))

View File

@ -1,4 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -52,13 +54,48 @@ class _PostContentHeader extends StatelessWidget {
], ],
), ),
), ),
IconButton( PopupMenuButton(
onPressed: () {}, icon: const Icon(Symbols.more_horiz),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4), style: const ButtonStyle(
icon: const Icon( visualDensity: VisualDensity(horizontal: -4, vertical: -4),
Symbols.more_horiz,
size: 16,
), ),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'postEditor',
pathParameters: {'mode': data.typePlural},
queryParameters: {'editing': data.id.toString()},
);
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
),
const PopupMenuDivider(),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.flag),
const Gap(16),
Text('report').tr(),
],
),
),
],
), ),
], ],
).padding(horizontal: 12, vertical: 8); ).padding(horizontal: 12, vertical: 8);

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -67,7 +68,7 @@ class UniversalImage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
AnimateWidgetExtensions(Icon(Icons.close, size: 24)) AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true)) .animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms), .fade(duration: 500.ms),
Text( Text(
@ -123,7 +124,7 @@ class UniversalImage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
AnimateWidgetExtensions(Icon(Icons.close, size: 24)) AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true)) .animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms), .fade(duration: 500.ms),
Text( Text(

View File

@ -1123,7 +1123,7 @@ packages:
source: hosted source: hosted
version: "0.28.0" version: "0.28.0"
shared_preferences: shared_preferences:
dependency: transitive dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"

View File

@ -70,6 +70,7 @@ dependencies:
dismissible_page: ^1.0.2 dismissible_page: ^1.0.2
uuid: ^4.5.1 uuid: ^4.5.1
photo_view: ^0.15.0 photo_view: ^0.15.0
shared_preferences: ^2.3.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:surface/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const SolianApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}