♻️ Refactored using controller as post editing

This commit is contained in:
LittleSheep 2024-11-11 21:30:05 +08:00
parent 1ff4dc2a4b
commit f23ffe61f5
12 changed files with 851 additions and 558 deletions

View File

@ -37,6 +37,7 @@
"report": "Report", "report": "Report",
"repost": "Repost", "repost": "Repost",
"reply": "Reply", "reply": "Reply",
"unset": "Unset",
"untitled": "Untitled", "untitled": "Untitled",
"postDetail": "Post detail", "postDetail": "Post detail",
"postNoun": "Post", "postNoun": "Post",
@ -82,6 +83,8 @@
"fieldPostTitle": "Title", "fieldPostTitle": "Title",
"fieldPostDescription": "Description", "fieldPostDescription": "Description",
"postPublish": "Publish", "postPublish": "Publish",
"postPublishedAt": "Published At",
"postPublishedUntil": "Published Until",
"postEditingNotice": "You're about to editing a post that posted {}.", "postEditingNotice": "You're about to editing a post that posted {}.",
"postReplyingNotice": "You're about to reply to a post that posted {}.", "postReplyingNotice": "You're about to reply to a post that posted {}.",
"postRepostingNotice": "You're about to repost a post that posted {}.", "postRepostingNotice": "You're about to repost a post that posted {}.",

View File

@ -37,6 +37,7 @@
"report": "检举", "report": "检举",
"repost": "转帖", "repost": "转帖",
"reply": "回贴", "reply": "回贴",
"unset": "未设置",
"untitled": "无题", "untitled": "无题",
"postDetail": "帖子详情", "postDetail": "帖子详情",
"postNoun": "帖子", "postNoun": "帖子",
@ -82,6 +83,8 @@
"fieldPostTitle": "标题", "fieldPostTitle": "标题",
"fieldPostDescription": "描述", "fieldPostDescription": "描述",
"postPublish": "发布", "postPublish": "发布",
"postPublishedAt": "发布于",
"postPublishedUntil": "取消发布于",
"postEditingNotice": "你正在修改由 {} 发布的帖子。", "postEditingNotice": "你正在修改由 {} 发布的帖子。",
"postReplyingNotice": "你正在回复由 {} 发布的帖子。", "postReplyingNotice": "你正在回复由 {} 发布的帖子。",
"postRepostingNotice": "你正在转发由 {} 发布的帖子。", "postRepostingNotice": "你正在转发由 {} 发布的帖子。",

View File

@ -0,0 +1,354 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart';
enum PostWriteMediaType {
image,
video,
audio,
file,
}
class PostWriteMedia {
late String name;
late PostWriteMediaType type;
final SnAttachment? attachment;
final XFile? file;
final Uint8List? raw;
PostWriteMedia(this.attachment, {this.file, this.raw}) {
name = attachment!.name;
switch (attachment?.mimetype.split('/').firstOrNull) {
case 'image':
type = PostWriteMediaType.image;
break;
case 'video':
type = PostWriteMediaType.video;
break;
case 'audio':
type = PostWriteMediaType.audio;
break;
default:
type = PostWriteMediaType.file;
}
}
PostWriteMedia.fromFile(this.file, {this.attachment, this.raw}) {
name = file!.name;
switch (file?.mimeType?.split('/').firstOrNull) {
case 'image':
type = PostWriteMediaType.image;
break;
case 'video':
type = PostWriteMediaType.video;
break;
case 'audio':
type = PostWriteMediaType.audio;
break;
default:
type = PostWriteMediaType.file;
}
}
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
{this.attachment, this.file});
bool get isEmpty => attachment == null && file == null && raw == null;
Future<int?> length() async {
if (attachment != null) {
return attachment!.size;
} else if (file != null) {
return await file!.length();
} else if (raw != null) {
return raw!.length;
}
return null;
}
XFile? toFile() {
if (file != null) {
return file!;
} else if (raw != null) {
return XFile.fromData(raw!, name: name);
}
return null;
}
ImageProvider? getImageProvider(
BuildContext context, {
int? width,
int? height,
}) {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
return UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
} else if (file != null) {
final ImageProvider provider =
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
if (width != null && height != null) {
return ResizeImage(
provider,
width: width,
height: height,
policy: ResizeImagePolicy.fit,
);
}
return provider;
} else if (raw != null) {
final provider = MemoryImage(raw!);
if (width != null && height != null) {
return ResizeImage(
provider,
width: width,
height: height,
policy: ResizeImagePolicy.fit,
);
}
return provider;
}
return null;
}
}
class PostWriteController extends ChangeNotifier {
static const Map<String, String> kTitleMap = {
'stories': 'writePostTypeStory',
'articles': 'writePostTypeArticle',
};
static const kAttachmentProgressWeight = 0.9;
static const kPostingProgressWeight = 0.1;
final TextEditingController contentController = TextEditingController();
final TextEditingController titleController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
PostWriteController() {
titleController.addListener(() => notifyListeners());
descriptionController.addListener(() => notifyListeners());
}
String mode = kTitleMap.keys.first;
String get title => titleController.text;
String get description => descriptionController.text;
bool get isRelatedNull =>
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
bool isLoading = false, isBusy = false;
double? progress;
SnPublisher? publisher;
SnPost? editingPost, repostingPost, replyingPost;
List<PostWriteMedia> attachments = List.empty(growable: true);
DateTime? publishedAt, publishedUntil;
Future<void> fetchRelatedPost(
BuildContext context, {
int? editing,
int? reposting,
int? replying,
}) async {
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
isLoading = true;
notifyListeners();
try {
if (editing != null) {
final resp = await sn.client.get('/cgi/co/posts/$editing');
final post = SnPost.fromJson(resp.data);
final alts = await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []);
publisher = post.publisher;
titleController.text = post.body['title'] ?? '';
descriptionController.text = post.body['description'] ?? '';
contentController.text = post.body['content'] ?? '';
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
attachments.addAll(alts.map((ele) => PostWriteMedia(ele)));
editingPost = post.copyWith(
preload: SnPostPreload(
attachments: alts,
),
);
}
if (replying != null) {
final resp = await sn.client.get('/cgi/co/posts/$replying');
final post = SnPost.fromJson(resp.data);
replyingPost = post.copyWith(
preload: SnPostPreload(
attachments: await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
),
);
}
if (reposting != null) {
final resp = await sn.client.get('/cgi/co/posts/$reposting');
final post = SnPost.fromJson(resp.data);
repostingPost = post.copyWith(
preload: SnPostPreload(
attachments: await attach
.getMultiple(post.body['attachments']?.cast<String>() ?? []),
),
);
}
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
} finally {
isLoading = false;
notifyListeners();
}
}
void post(BuildContext context) async {
if (isBusy || publisher == null) return;
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
progress = 0;
isBusy = true;
notifyListeners();
// Uploading attachments
try {
for (int i = 0; i < attachments.length; i++) {
final media = attachments[i];
if (media.attachment != null) continue; // Already uploaded, skip
if (media.isEmpty) continue; // Nothing to do, skip
final place = await attach.chunkedUploadInitialize(
(await media.length())!,
media.name,
'interactive',
null,
);
final item = await attach.chunkedUploadParts(
media.toFile()!,
place.$1,
place.$2,
onProgress: (progress) {
// Calculate overall progress for attachments
progress = ((i + progress) / attachments.length) *
kAttachmentProgressWeight;
notifyListeners();
},
);
attachments[i] = PostWriteMedia(item);
}
} catch (err) {
isBusy = false;
notifyListeners();
if (!context.mounted) return;
context.showErrorDialog(err);
return;
}
progress = kAttachmentProgressWeight;
notifyListeners();
// Posting the content
try {
final baseProgressVal = progress!;
await sn.client.request(
[
'/cgi/co/$mode',
if (editingPost != null) '${editingPost!.id}',
].join('/'),
data: {
'publisher': publisher!.id,
'content': contentController.text,
'title': titleController.text,
'description': descriptionController.text,
'attachments': attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
if (publishedAt != null)
'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null)
'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.id,
if (repostingPost != null) 'repost_to': repostingPost!.id,
},
onSendProgress: (count, total) {
progress =
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
onReceiveProgress: (count, total) {
progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
options: Options(
method: editingPost != null ? 'PUT' : 'POST',
),
);
if (!context.mounted) return;
Navigator.pop(context, true);
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
} finally {
isBusy = false;
notifyListeners();
}
}
void addAttachments(Iterable<PostWriteMedia> items) {
attachments.addAll(items);
notifyListeners();
}
void removeAttachmentAt(int idx) {
attachments.removeAt(idx);
notifyListeners();
}
void setPublisher(SnPublisher? item) {
publisher = item;
notifyListeners();
}
void setPublishedAt(DateTime? value) {
publishedAt = value;
notifyListeners();
}
void setPublishedUntil(DateTime? value) {
publishedUntil = value;
notifyListeners();
}
@override
void dispose() {
contentController.dispose();
titleController.dispose();
descriptionController.dispose();
super.dispose();
}
}

View File

@ -1,7 +1,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
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';
@ -11,9 +10,8 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.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/loading_indicator.dart';
@ -42,203 +40,37 @@ class PostEditorScreen extends StatefulWidget {
} }
class _PostEditorScreenState extends State<PostEditorScreen> { class _PostEditorScreenState extends State<PostEditorScreen> {
static const Map<String, String> _kTitleMap = { final PostWriteController _writeController = PostWriteController();
'stories': 'writePostTypeStory',
'articles': 'writePostTypeArticle',
};
bool _isBusy = false; bool _isFetching = false;
bool _isLoading = false; bool get _isLoading => _isFetching || _writeController.isLoading;
SnPublisher? _publisher;
List<SnPublisher>? _publishers; List<SnPublisher>? _publishers;
final List<XFile> _selectedMedia = List.empty(growable: true);
final List<SnAttachment> _attachments = List.empty(growable: true);
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
setState(() => _isFetching = true);
try {
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(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
setState(() { _writeController.setPublisher(_publishers?.firstOrNull);
_publisher = _publishers?.first;
});
}
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? _description;
final TextEditingController _contentController = TextEditingController();
double? _progress;
static const kAttachmentProgressWeight = 0.9;
static const kPostingProgressWeight = 0.1;
void _performAction() async {
if (_isBusy || _publisher == null) return;
final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
setState(() {
_progress = 0;
_isBusy = true;
});
// Uploading attachments
try {
for (int i = 0; i < _selectedMedia.length; i++) {
final media = _selectedMedia[i];
final place = await attach.chunkedUploadInitialize(
await media.length(),
media.name,
'interactive',
null,
);
final item = await attach.chunkedUploadParts(
media,
place.$1,
place.$2,
onProgress: (progress) {
// Calculate overall progress for attachments
setState(() {
_progress = ((i + progress) / _selectedMedia.length) *
kAttachmentProgressWeight;
});
},
);
_attachments.add(item);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
return;
}
setState(() => _progress = kAttachmentProgressWeight);
// Posting the content
try {
final baseProgressVal = _progress!;
await sn.client.request(
[
'/cgi/co/${widget.mode}',
if (widget.postEditId != null) '${widget.postEditId}',
].join('/'),
data: {
'publisher': _publisher!.id,
'content': _contentController.value.text,
'title': _title,
'description': _description,
'attachments': _attachments.map((e) => e.rid).toList(),
if (_replyingTo != null) 'reply_to': _replyingTo!.id,
if (_repostingTo != null) 'repost_to': _repostingTo!.id,
},
onSendProgress: (count, total) {
setState(() {
_progress = baseProgressVal +
(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;
Navigator.pop(context, true);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isFetching = false);
} }
} }
void _updateMeta() { void _updateMeta() {
showModalBottomSheet<PostMetaResult?>( showModalBottomSheet(
context: context, context: context,
builder: (context) => PostMetaEditor( builder: (context) => PostMetaEditor(controller: _writeController),
initialTitle: _title,
initialDescription: _description,
),
useRootNavigator: true, useRootNavigator: true,
).then((value) { );
if (value is PostMetaResult) {
_title = value.title;
_description = value.description;
setState(() {});
}
});
} }
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
@ -246,29 +78,39 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
void _selectMedia() async { void _selectMedia() async {
final result = await _imagePicker.pickMultipleMedia(); final result = await _imagePicker.pickMultipleMedia();
if (result.isEmpty) return; if (result.isEmpty) return;
_selectedMedia.addAll(result); _writeController.addAttachments(
result.map((e) => PostWriteMedia.fromFile(e)),
);
setState(() {}); setState(() {});
} }
@override @override
void dispose() { void dispose() {
_contentController.dispose(); _writeController.dispose();
super.dispose(); super.dispose();
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (!_kTitleMap.keys.contains(widget.mode)) { if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
context.showErrorDialog('Unknown post type'); context.showErrorDialog('Unknown post type');
Navigator.pop(context); Navigator.pop(context);
} }
_fetchRelatedPost();
_fetchPublishers(); _fetchPublishers();
_writeController.fetchRelatedPost(
context,
editing: widget.postEditId,
replying: widget.postReplyId,
reposting: widget.postRepostId,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _writeController,
builder: (context, _) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
@ -278,10 +120,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
flexibleSpace: Column( flexibleSpace: Column(
children: [ children: [
Text(_title ?? 'untitled'.tr()) Text(_writeController.title.isNotEmpty
? _writeController.title
: 'untitled'.tr())
.textStyle(Theme.of(context).textTheme.titleLarge!) .textStyle(Theme.of(context).textTheme.titleLarge!)
.textColor(Colors.white), .textColor(Colors.white),
Text(_kTitleMap[widget.mode]!) Text(PostWriteController.kTitleMap[widget.mode]!)
.tr() .tr()
.textColor(Colors.white.withAlpha((255 * 0.9).round())), .textColor(Colors.white.withAlpha((255 * 0.9).round())),
], ],
@ -289,7 +133,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.tune), icon: const Icon(Symbols.tune),
onPressed: _isBusy ? null : _updateMeta, onPressed: _writeController.isBusy ? null : _updateMeta,
), ),
], ],
), ),
@ -308,7 +152,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, enabled: _writeController.editingPost == null,
value: item, value: item,
child: Row( child: Row(
children: [ children: [
@ -317,10 +161,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text(item.nick).textStyle( Text(item.nick).textStyle(
Theme.of(context).textTheme.bodyMedium!), Theme.of(context)
.textTheme
.bodyMedium!),
Text('@${item.name}') Text('@${item.name}')
.textStyle(Theme.of(context) .textStyle(Theme.of(context)
.textTheme .textTheme
@ -360,22 +207,19 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
], ],
value: _publisher, value: _writeController.publisher,
onChanged: (SnPublisher? value) { onChanged: (SnPublisher? value) {
if (value == null) { if (value == null) {
GoRouter.of(context) GoRouter.of(context)
.pushNamed('accountPublisherNew') .pushNamed('accountPublisherNew')
.then((value) { .then((value) {
if (value == true) { if (value == true) {
_publisher = null;
_publishers = null; _publishers = null;
_fetchPublishers(); _fetchPublishers();
} }
}); });
} else { } else {
setState(() { _writeController.setPublisher(value);
_publisher = value;
});
} }
}, },
buttonStyleData: const ButtonStyleData( buttonStyleData: const ButtonStyleData(
@ -394,7 +238,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
children: [ children: [
// Replying Notice // Replying Notice
if (_replyingTo != null) if (_writeController.replyingPost != null)
Column( Column(
children: [ children: [
Theme( Theme(
@ -403,18 +247,23 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
child: ExpansionTile( child: ExpansionTile(
minTileHeight: 48, minTileHeight: 48,
leading: const Icon(Symbols.reply).padding(left: 4), leading:
const Icon(Symbols.reply).padding(left: 4),
title: Text('postReplyingNotice') title: Text('postReplyingNotice')
.fontSize(15) .fontSize(15)
.tr(args: ['@${_replyingTo!.publisher.name}']), .tr(args: [
children: <Widget>[PostItem(data: _replyingTo!)], '@${_writeController.replyingPost!.publisher.name}'
]),
children: <Widget>[
PostItem(data: _writeController.replyingPost!)
],
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
], ],
), ),
// Reposting Notice // Reposting Notice
if (_repostingTo != null) if (_writeController.repostingPost != null)
Column( Column(
children: [ children: [
Theme( Theme(
@ -423,19 +272,24 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
child: ExpansionTile( child: ExpansionTile(
minTileHeight: 48, minTileHeight: 48,
leading: leading: const Icon(Symbols.forward)
const Icon(Symbols.forward).padding(left: 4), .padding(left: 4),
title: Text('postRepostingNotice') title: Text('postRepostingNotice')
.fontSize(15) .fontSize(15)
.tr(args: ['@${_repostingTo!.publisher.name}']), .tr(args: [
children: <Widget>[PostItem(data: _repostingTo!)], '@${_writeController.repostingPost!.publisher.name}'
]),
children: <Widget>[
PostItem(
data: _writeController.repostingPost!)
],
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
], ],
), ),
// Editing Notice // Editing Notice
if (_editingOg != null) if (_writeController.editingPost != null)
Column( Column(
children: [ children: [
Theme( Theme(
@ -444,12 +298,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
child: ExpansionTile( child: ExpansionTile(
minTileHeight: 48, minTileHeight: 48,
leading: leading: const Icon(Symbols.edit_note)
const Icon(Symbols.edit_note).padding(left: 4), .padding(left: 4),
title: Text('postEditingNotice') title: Text('postEditingNotice')
.fontSize(15) .fontSize(15)
.tr(args: ['@${_editingOg!.publisher.name}']), .tr(args: [
children: <Widget>[PostItem(data: _editingOg!)], '@${_writeController.editingPost!.publisher.name}'
]),
children: <Widget>[
PostItem(data: _writeController.editingPost!)
],
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
@ -457,7 +315,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
// Content Input Area // Content Input Area
TextField( TextField(
controller: _contentController, controller: _writeController.contentController,
maxLines: null, maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'fieldPostContent'.tr(), hintText: 'fieldPostContent'.tr(),
@ -474,9 +332,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
] ]
.expandIndexed( .expandIndexed(
(idx, ele) => [ (idx, ele) => [
if (idx != 0 || if (idx != 0 || _writeController.isRelatedNull)
![_editingOg, _replyingTo, _repostingTo]
.any((x) => x != null))
const Gap(8), const Gap(8),
ele, ele,
], ],
@ -485,13 +341,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
), ),
if (_selectedMedia.isNotEmpty) if (_writeController.attachments.isNotEmpty)
PostMediaPendingList( PostMediaPendingList(
data: _selectedMedia, data: _writeController.attachments,
onRemove: (idx) { onRemove: (idx) {
setState(() { _writeController.removeAttachmentAt(idx);
_selectedMedia.removeAt(idx);
});
}, },
).padding(bottom: 8), ).padding(bottom: 8),
Material( Material(
@ -500,14 +354,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LoadingIndicator(isActive: _isLoading), LoadingIndicator(isActive: _isLoading),
if (_isBusy && _progress != null) if (_writeController.isBusy &&
_writeController.progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1), tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
builder: (context, value, _) => builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2), LinearProgressIndicator(value: value, minHeight: 2),
) )
else if (_isBusy) else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2), const LinearProgressIndicator(value: null, minHeight: 2),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -520,10 +375,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
onPressed: _isBusy ? null : _selectMedia, onPressed: _writeController.isBusy
? null
: _selectMedia,
icon: Icon( icon: Icon(
Symbols.add_photo_alternate, Symbols.add_photo_alternate,
color: Theme.of(context).colorScheme.primary, color:
Theme.of(context).colorScheme.primary,
), ),
), ),
], ],
@ -532,9 +390,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
TextButton.icon( TextButton.icon(
onPressed: (_isBusy || _publisher == null) onPressed: (_writeController.isBusy ||
_writeController.publisher == null)
? null ? null
: _performAction, : () => _writeController.post(context),
icon: const Icon(Symbols.send), icon: const Icon(Symbols.send),
label: Text('postPublish').tr(), label: Text('postPublish').tr(),
), ),
@ -549,6 +408,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
], ],
), ),
); );
},
);
} }
} }

View File

@ -34,7 +34,7 @@ class SnPost with _$SnPost {
required DateTime? lockedAt, required DateTime? lockedAt,
required bool isDraft, required bool isDraft,
required DateTime? publishedAt, required DateTime? publishedAt,
required dynamic publishedUntil, required DateTime? publishedUntil,
required int totalUpvote, required int totalUpvote,
required int totalDownvote, required int totalDownvote,
required int? realmId, required int? realmId,

View File

@ -45,7 +45,7 @@ mixin _$SnPost {
DateTime? get lockedAt => throw _privateConstructorUsedError; DateTime? get lockedAt => throw _privateConstructorUsedError;
bool get isDraft => throw _privateConstructorUsedError; bool get isDraft => throw _privateConstructorUsedError;
DateTime? get publishedAt => throw _privateConstructorUsedError; DateTime? get publishedAt => throw _privateConstructorUsedError;
dynamic get publishedUntil => throw _privateConstructorUsedError; DateTime? get publishedUntil => throw _privateConstructorUsedError;
int get totalUpvote => throw _privateConstructorUsedError; int get totalUpvote => throw _privateConstructorUsedError;
int get totalDownvote => throw _privateConstructorUsedError; int get totalDownvote => throw _privateConstructorUsedError;
int? get realmId => throw _privateConstructorUsedError; int? get realmId => throw _privateConstructorUsedError;
@ -95,7 +95,7 @@ abstract class $SnPostCopyWith<$Res> {
DateTime? lockedAt, DateTime? lockedAt,
bool isDraft, bool isDraft,
DateTime? publishedAt, DateTime? publishedAt,
dynamic publishedUntil, DateTime? publishedUntil,
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
@ -264,7 +264,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
publishedUntil: freezed == publishedUntil publishedUntil: freezed == publishedUntil
? _value.publishedUntil ? _value.publishedUntil
: publishedUntil // ignore: cast_nullable_to_non_nullable : publishedUntil // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
totalUpvote: null == totalUpvote totalUpvote: null == totalUpvote
? _value.totalUpvote ? _value.totalUpvote
: totalUpvote // ignore: cast_nullable_to_non_nullable : totalUpvote // ignore: cast_nullable_to_non_nullable
@ -368,7 +368,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
DateTime? lockedAt, DateTime? lockedAt,
bool isDraft, bool isDraft,
DateTime? publishedAt, DateTime? publishedAt,
dynamic publishedUntil, DateTime? publishedUntil,
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
@ -538,7 +538,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
publishedUntil: freezed == publishedUntil publishedUntil: freezed == publishedUntil
? _value.publishedUntil ? _value.publishedUntil
: publishedUntil // ignore: cast_nullable_to_non_nullable : publishedUntil // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
totalUpvote: null == totalUpvote totalUpvote: null == totalUpvote
? _value.totalUpvote ? _value.totalUpvote
: totalUpvote // ignore: cast_nullable_to_non_nullable : totalUpvote // ignore: cast_nullable_to_non_nullable
@ -690,7 +690,7 @@ class _$SnPostImpl extends _SnPost {
@override @override
final DateTime? publishedAt; final DateTime? publishedAt;
@override @override
final dynamic publishedUntil; final DateTime? publishedUntil;
@override @override
final int totalUpvote; final int totalUpvote;
@override @override
@ -756,8 +756,8 @@ class _$SnPostImpl extends _SnPost {
(identical(other.isDraft, isDraft) || other.isDraft == isDraft) && (identical(other.isDraft, isDraft) || other.isDraft == isDraft) &&
(identical(other.publishedAt, publishedAt) || (identical(other.publishedAt, publishedAt) ||
other.publishedAt == publishedAt) && other.publishedAt == publishedAt) &&
const DeepCollectionEquality() (identical(other.publishedUntil, publishedUntil) ||
.equals(other.publishedUntil, publishedUntil) && other.publishedUntil == publishedUntil) &&
(identical(other.totalUpvote, totalUpvote) || (identical(other.totalUpvote, totalUpvote) ||
other.totalUpvote == totalUpvote) && other.totalUpvote == totalUpvote) &&
(identical(other.totalDownvote, totalDownvote) || (identical(other.totalDownvote, totalDownvote) ||
@ -801,7 +801,7 @@ class _$SnPostImpl extends _SnPost {
lockedAt, lockedAt,
isDraft, isDraft,
publishedAt, publishedAt,
const DeepCollectionEquality().hash(publishedUntil), publishedUntil,
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
realmId, realmId,
@ -855,7 +855,7 @@ abstract class _SnPost extends SnPost {
required final DateTime? lockedAt, required final DateTime? lockedAt,
required final bool isDraft, required final bool isDraft,
required final DateTime? publishedAt, required final DateTime? publishedAt,
required final dynamic publishedUntil, required final DateTime? publishedUntil,
required final int totalUpvote, required final int totalUpvote,
required final int totalDownvote, required final int totalDownvote,
required final int? realmId, required final int? realmId,
@ -919,7 +919,7 @@ abstract class _SnPost extends SnPost {
@override @override
DateTime? get publishedAt; DateTime? get publishedAt;
@override @override
dynamic get publishedUntil; DateTime? get publishedUntil;
@override @override
int get totalUpvote; int get totalUpvote;
@override @override

View File

@ -42,7 +42,9 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
publishedAt: json['published_at'] == null publishedAt: json['published_at'] == null
? null ? null
: DateTime.parse(json['published_at'] as String), : DateTime.parse(json['published_at'] as String),
publishedUntil: json['published_until'], publishedUntil: json['published_until'] == null
? null
: DateTime.parse(json['published_until'] as String),
totalUpvote: (json['total_upvote'] as num).toInt(), totalUpvote: (json['total_upvote'] as num).toInt(),
totalDownvote: (json['total_downvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(),
realmId: (json['realm_id'] as num?)?.toInt(), realmId: (json['realm_id'] as num?)?.toInt(),
@ -83,7 +85,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
'locked_at': instance.lockedAt?.toIso8601String(), 'locked_at': instance.lockedAt?.toIso8601String(),
'is_draft': instance.isDraft, 'is_draft': instance.isDraft,
'published_at': instance.publishedAt?.toIso8601String(), 'published_at': instance.publishedAt?.toIso8601String(),
'published_until': instance.publishedUntil, 'published_until': instance.publishedUntil?.toIso8601String(),
'total_upvote': instance.totalUpvote, 'total_upvote': instance.totalUpvote,
'total_downvote': instance.totalDownvote, 'total_downvote': instance.totalDownvote,
'realm_id': instance.realmId, 'realm_id': instance.realmId,

View File

@ -35,6 +35,7 @@ class AccountImage extends StatelessWidget {
UniversalImage.provider(url), UniversalImage.provider(url),
width: ((radius ?? 20) * devicePixelRatio * 2).round(), width: ((radius ?? 20) * devicePixelRatio * 2).round(),
height: ((radius ?? 20) * devicePixelRatio * 2).round(), height: ((radius ?? 20) * devicePixelRatio * 2).round(),
policy: ResizeImagePolicy.fit,
) )
: null, : null,
child: (content?.isEmpty ?? true) child: (content?.isEmpty ?? true)

View File

@ -1,15 +1,13 @@
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
class PostMediaPendingList extends StatelessWidget { class PostMediaPendingList extends StatelessWidget {
final List<XFile> data; final List<PostWriteMedia> data;
final Function(int idx)? onRemove; final Function(int idx)? onRemove;
const PostMediaPendingList({ const PostMediaPendingList({
super.key, super.key,
@ -19,6 +17,8 @@ class PostMediaPendingList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Container( return Container(
constraints: const BoxConstraints(maxHeight: 120), constraints: const BoxConstraints(maxHeight: 120),
child: ListView.separated( child: ListView.separated(
@ -53,9 +53,25 @@ class PostMediaPendingList extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: kIsWeb child: switch (file.type) {
? Image.network(file.path, fit: BoxFit.cover) PostWriteMediaType.image =>
: Image.file(File(file.path), fit: BoxFit.cover), LayoutBuilder(builder: (context, constraints) {
return Image(
image: file.getImageProvider(
context,
width: (constraints.maxWidth * devicePixelRatio)
.round(),
height: (constraints.maxHeight * devicePixelRatio)
.round(),
)!,
fit: BoxFit.cover,
);
}),
_ => Container(
color: Theme.of(context).colorScheme.surface,
child: const Icon(Symbols.docs).center(),
),
},
), ),
), ),
), ),

View File

@ -1,87 +1,130 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/post_write_controller.dart';
class PostMetaResult { class PostMetaEditor extends StatelessWidget {
final String title; final PostWriteController controller;
final String description; const PostMetaEditor({super.key, required this.controller});
PostMetaResult({required this.title, required this.description}); Future<DateTime?> _selectDate(
} BuildContext context, {
DateTime? initialDateTime,
class PostMetaEditor extends StatefulWidget { }) async {
final String? initialTitle; DateTime? picked;
final String? initialDescription; await showCupertinoModalPopup(
const PostMetaEditor({super.key, this.initialTitle, this.initialDescription}); context: context,
builder: (BuildContext context) => Container(
@override height: 216,
State<PostMetaEditor> createState() => _PostMetaEditorState(); padding: const EdgeInsets.only(top: 6.0),
} margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
class _PostMetaEditorState extends State<PostMetaEditor> { ),
final TextEditingController _titleController = TextEditingController(); color: Theme.of(context).colorScheme.surface,
final TextEditingController _descriptionController = TextEditingController(); child: SafeArea(
top: false,
void _applyChanges() { child: CupertinoDatePicker(
Navigator.pop( initialDateTime: initialDateTime,
context, mode: CupertinoDatePickerMode.dateAndTime,
PostMetaResult( use24hFormat: true,
title: _titleController.text, onDateTimeChanged: (DateTime newDate) {
description: _descriptionController.text, picked = newDate;
},
),
),
), ),
); );
} return picked;
@override
void initState() {
super.initState();
_titleController.text = widget.initialTitle ?? '';
_descriptionController.text = widget.initialDescription ?? '';
}
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dateFormatter = DateFormat('y/M/d HH:mm:ss');
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
return Column( return Column(
children: [ children: [
TextField( TextField(
controller: _titleController, controller: controller.titleController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldPostTitle'.tr(), labelText: 'fieldPostTitle'.tr(),
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
), FocusManager.instance.primaryFocus?.unfocus(),
const Gap(4), ).padding(horizontal: 24),
if (controller.mode == 'article') const Gap(4),
if (controller.mode == 'article')
TextField( TextField(
controller: _descriptionController, controller: controller.descriptionController,
maxLines: null, maxLines: null,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'fieldPostDescription'.tr(), labelText: 'fieldPostDescription'.tr(),
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
), FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 24),
const Gap(12), const Gap(12),
Row( ListTile(
mainAxisAlignment: MainAxisAlignment.end, leading: const Icon(Symbols.event_available),
children: [ title: Text('postPublishedAt').tr(),
ElevatedButton.icon( subtitle: Text(
onPressed: _applyChanges, controller.publishedAt != null
icon: const Icon(Symbols.save), ? dateFormatter.format(controller.publishedAt!)
label: Text('apply').tr(), : 'unset'.tr(),
),
trailing: controller.publishedAt != null
? IconButton(
icon: const Icon(Symbols.cancel),
onPressed: () {
controller.setPublishedAt(null);
},
)
: null,
contentPadding: const EdgeInsets.only(left: 24, right: 18),
onTap: () {
_selectDate(
context,
initialDateTime: controller.publishedAt,
).then((value) {
controller.setPublishedAt(value);
});
},
),
ListTile(
leading: const Icon(Symbols.event_busy),
title: Text('postPublishedUntil').tr(),
subtitle: Text(
controller.publishedUntil != null
? dateFormatter.format(controller.publishedUntil!)
: 'unset'.tr(),
),
trailing: controller.publishedUntil != null
? IconButton(
icon: const Icon(Symbols.cancel),
onPressed: () {
controller.setPublishedUntil(null);
},
)
: null,
contentPadding: const EdgeInsets.only(left: 24, right: 18),
onTap: () {
_selectDate(
context,
initialDateTime: controller.publishedUntil,
).then((value) {
controller.setPublishedUntil(value);
});
},
), ),
], ],
) ).padding(vertical: 8);
], },
).padding(horizontal: 24, vertical: 8); );
} }
} }

View File

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
class PostMiniEditor extends StatelessWidget {
const PostMiniEditor({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -5,23 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "72.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
source: sdk source: sdk
version: "0.3.3" version: "0.3.2"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "6.7.0"
animations: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@ -202,10 +202,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.0" version: "1.18.0"
connectivity_plus: connectivity_plus:
dependency: transitive dependency: transitive
description: description:
@ -790,18 +790,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.8" version: "10.0.5"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.9" version: "3.0.5"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -838,10 +838,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: macros name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3-main.0" version: "0.1.2-main.4"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1150,7 +1150,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.99"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -1227,10 +1227,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -1251,10 +1251,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.2.0"
styled_widget: styled_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1291,10 +1291,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.2"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1411,10 +1411,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.0" version: "14.2.5"
watcher: watcher:
dependency: transitive dependency: transitive
description: description: