Compare commits
18 Commits
54c098c274
...
2.4.2+78
Author | SHA1 | Date | |
---|---|---|---|
88396647f3 | |||
335318ae3f | |||
da25fb9c29 | |||
c1aef89b84 | |||
0241c5f804 | |||
f6939d7c23 | |||
d654c162e3 | |||
25550ba197 | |||
3defd3a593 | |||
d62ed4c375 | |||
857f3cc832 | |||
e16bc80eea | |||
a4f6e8af56 | |||
060a97f5ec | |||
92f7e92018 | |||
5c483bd3b8 | |||
1c510d63fe | |||
115cb4adc1 |
@ -153,6 +153,11 @@
|
||||
"publisherRunBy": "Run by {}",
|
||||
"fieldPublisherBelongToRealm": "Belongs to",
|
||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||
"writePost": "Compose",
|
||||
"postTypeStory": "Story",
|
||||
"postTypeArticle": "Article",
|
||||
"postTypeQuestion": "Question",
|
||||
"postTypeVideo": "Video",
|
||||
"writePostTypeStory": "Post a story",
|
||||
"writePostTypeArticle": "Write an article",
|
||||
"writePostTypeQuestion": "Ask a question",
|
||||
@ -763,5 +768,28 @@
|
||||
"decrypting": "Decrypting……",
|
||||
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online",
|
||||
"messageUnablePreview": "Unable preview",
|
||||
"messageUnablePreviewEncrypted": "Unable preview encrypted message"
|
||||
"messageUnablePreviewEncrypted": "Unable preview encrypted message",
|
||||
"postViewInGlobalDescription": "Do not view the post in the specific realm.",
|
||||
"postDraftSaved": "The draft has been saved.",
|
||||
"postDraftBox": "Draft Box",
|
||||
"postShuffle": "Read Randomly",
|
||||
"checkInStreak": {
|
||||
"zero": "No streak",
|
||||
"one": "{} day streak",
|
||||
"other": "{} days streak"
|
||||
},
|
||||
"accountChangeStatus": "Change Status",
|
||||
"accountStatusSilent": "Do not Disturb",
|
||||
"accountStatusSilentDesc": "The notification will stop popping up",
|
||||
"accountStatusInvisible": "Invisible",
|
||||
"accountStatusInvisibleDesc": "Will show as offline, but all features still remain normal",
|
||||
"accountCustomStatus": "Custom Status",
|
||||
"accountCustomStatusDescription": "Customize your status.",
|
||||
"accountClearStatus": "Clear Status",
|
||||
"accountClearStatusDescription": "Clear your status, and let server decide which status you are for you.",
|
||||
"fieldAccountStatusLabel": "Status Text",
|
||||
"fieldAccountStatusClearAt": "Clear At",
|
||||
"accountStatusNegative": "Negative",
|
||||
"accountStatusNeutral": "Neutral",
|
||||
"accountStatusPositive": "Positive"
|
||||
}
|
||||
|
@ -137,6 +137,11 @@
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所属领域",
|
||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||
"writePost": "撰写",
|
||||
"postTypeStory": "动态",
|
||||
"postTypeArticle": "文章",
|
||||
"postTypeQuestion": "问题",
|
||||
"postTypeVideo": "视频",
|
||||
"writePostTypeStory": "发动态",
|
||||
"writePostTypeArticle": "写文章",
|
||||
"writePostTypeQuestion": "提问题",
|
||||
@ -761,5 +766,28 @@
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线",
|
||||
"messageUnablePreview": "无法预览消息",
|
||||
"messageUnablePreviewEncrypted": "无法预览加密消息"
|
||||
"messageUnablePreviewEncrypted": "无法预览加密消息",
|
||||
"postViewInGlobalDescription": "不查看特定领域的帖子。",
|
||||
"postDraftSaved": "已保存为草稿。",
|
||||
"postDraftBox": "草稿箱",
|
||||
"postShuffle": "随便看看",
|
||||
"checkInStreak": {
|
||||
"zero": "无连击",
|
||||
"one": "连续签到 {} 天",
|
||||
"other": "连续签到 {} 天"
|
||||
},
|
||||
"accountChangeStatus": "修改状态",
|
||||
"accountStatusSilent": "请勿打扰",
|
||||
"accountStatusSilentDesc": "将会暂停所有通知推送",
|
||||
"accountStatusInvisible": "隐身",
|
||||
"accountStatusInvisibleDesc": "将会在他人界面显示离线,但不影响功能使用",
|
||||
"accountCustomStatus": "自定义状态",
|
||||
"accountCustomStatusDescription": "客制化你的状态。",
|
||||
"accountClearStatus": "清除状态",
|
||||
"accountClearStatusDescription": "清除你的状态,并让服务器决定你的状态。",
|
||||
"fieldAccountStatusLabel": "状态文字",
|
||||
"fieldAccountStatusClearAt": "清除时间",
|
||||
"accountStatusNegative": "负面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
}
|
||||
|
@ -137,6 +137,11 @@
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所屬領域",
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePost": "撰寫",
|
||||
"postTypeStory": "動態",
|
||||
"postTypeArticle": "文章",
|
||||
"postTypeQuestion": "問題",
|
||||
"postTypeVideo": "視頻",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
@ -761,5 +766,28 @@
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
||||
"messageUnablePreview": "無法預覽消息",
|
||||
"messageUnablePreviewEncrypted": "無法預覽加密消息"
|
||||
"messageUnablePreviewEncrypted": "無法預覽加密消息",
|
||||
"postViewInGlobalDescription": "不查看特定領域的帖子。",
|
||||
"postDraftSaved": "已保存為草稿。",
|
||||
"postDraftBox": "草稿箱",
|
||||
"postShuffle": "隨便看看",
|
||||
"checkInStreak": {
|
||||
"zero": "無連擊",
|
||||
"one": "連續簽到 {} 天",
|
||||
"other": "連續簽到 {} 天"
|
||||
},
|
||||
"accountChangeStatus": "修改狀態",
|
||||
"accountStatusSilent": "請勿打擾",
|
||||
"accountStatusSilentDesc": "將會暫停所有通知推送",
|
||||
"accountStatusInvisible": "隱身",
|
||||
"accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
|
||||
"accountCustomStatus": "自定義狀態",
|
||||
"accountCustomStatusDescription": "客製化你的狀態。",
|
||||
"accountClearStatus": "清除狀態",
|
||||
"accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
|
||||
"fieldAccountStatusLabel": "狀態文字",
|
||||
"fieldAccountStatusClearAt": "清除時間",
|
||||
"accountStatusNegative": "負面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
}
|
||||
|
@ -137,6 +137,11 @@
|
||||
"publisherRunBy": "由 {} 管理",
|
||||
"fieldPublisherBelongToRealm": "所屬領域",
|
||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||
"writePost": "撰寫",
|
||||
"postTypeStory": "動態",
|
||||
"postTypeArticle": "文章",
|
||||
"postTypeQuestion": "問題",
|
||||
"postTypeVideo": "視頻",
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
@ -761,5 +766,28 @@
|
||||
"decrypting": "解密中……",
|
||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
||||
"messageUnablePreview": "無法預覽消息",
|
||||
"messageUnablePreviewEncrypted": "無法預覽加密消息"
|
||||
"messageUnablePreviewEncrypted": "無法預覽加密消息",
|
||||
"postViewInGlobalDescription": "不查看特定領域的帖子。",
|
||||
"postDraftSaved": "已保存為草稿。",
|
||||
"postDraftBox": "草稿箱",
|
||||
"postShuffle": "隨便看看",
|
||||
"checkInStreak": {
|
||||
"zero": "無連擊",
|
||||
"one": "連續簽到 {} 天",
|
||||
"other": "連續簽到 {} 天"
|
||||
},
|
||||
"accountChangeStatus": "修改狀態",
|
||||
"accountStatusSilent": "請勿打擾",
|
||||
"accountStatusSilentDesc": "將會暫停所有通知推送",
|
||||
"accountStatusInvisible": "隱身",
|
||||
"accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
|
||||
"accountCustomStatus": "自定義狀態",
|
||||
"accountCustomStatusDescription": "客製化你的狀態。",
|
||||
"accountClearStatus": "清除狀態",
|
||||
"accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
|
||||
"fieldAccountStatusLabel": "狀態文字",
|
||||
"fieldAccountStatusClearAt": "清除時間",
|
||||
"accountStatusNegative": "負面",
|
||||
"accountStatusNeutral": "中性",
|
||||
"accountStatusPositive": "正面"
|
||||
}
|
||||
|
@ -71,7 +71,8 @@ class PostWriteMedia {
|
||||
}
|
||||
}
|
||||
|
||||
PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file});
|
||||
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
|
||||
{this.attachment, this.file});
|
||||
|
||||
bool get isEmpty => attachment == null && file == null && raw == null;
|
||||
|
||||
@ -105,7 +106,8 @@ class PostWriteMedia {
|
||||
}) {
|
||||
if (attachment != null) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
final ImageProvider provider =
|
||||
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
|
||||
if (width != null && height != null && !kIsWeb) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
@ -116,7 +118,8 @@ class PostWriteMedia {
|
||||
}
|
||||
return provider;
|
||||
} else if (file != null) {
|
||||
final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||
final ImageProvider provider =
|
||||
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
|
||||
if (width != null && height != null) {
|
||||
return ResizeImage(
|
||||
provider,
|
||||
@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier {
|
||||
final TextEditingController aliasController = TextEditingController();
|
||||
final TextEditingController rewardController = TextEditingController();
|
||||
|
||||
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
|
||||
ContentInsertionConfiguration get contentInsertionConfiguration =>
|
||||
ContentInsertionConfiguration(
|
||||
onContentInserted: (KeyboardInsertedContent content) {
|
||||
if (content.hasData) {
|
||||
addAttachments(
|
||||
[PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
|
||||
addAttachments([
|
||||
PostWriteMedia.fromBytes(content.data!,
|
||||
'attachmentInsertedImage'.tr(), SnMediaType.image)
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
String get description => descriptionController.text;
|
||||
|
||||
bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||
bool get isRelatedNull =>
|
||||
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
|
||||
|
||||
bool isLoading = false, isBusy = false;
|
||||
double? progress;
|
||||
@ -201,6 +208,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
SnRealm? realm;
|
||||
SnPublisher? publisher;
|
||||
SnPost? editingPost, repostingPost, replyingPost;
|
||||
bool editingDraft = false;
|
||||
|
||||
int visibility = 0;
|
||||
List<int> visibleUsers = List.empty();
|
||||
@ -237,14 +245,20 @@ class PostWriteController extends ChangeNotifier {
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
invisibleUsers =
|
||||
List.from(post.invisibleUsersList ?? [], growable: true);
|
||||
visibility = post.visibility;
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
categories =
|
||||
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(
|
||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
editingDraft = post.isDraft;
|
||||
|
||||
if (post.preload?.thumbnail != null &&
|
||||
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
}
|
||||
if (post.preload?.realm != null) {
|
||||
@ -272,7 +286,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||
Future<SnAttachment> _uploadAttachment(
|
||||
BuildContext context, PostWriteMedia media,
|
||||
{bool isCompressed = false}) async {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
|
||||
@ -281,7 +296,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||
? 'image/png'
|
||||
: null,
|
||||
);
|
||||
|
||||
var item = await attach.chunkedUploadParts(
|
||||
@ -297,9 +314,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
|
||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||
try {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
final compressedAttachment =
|
||||
await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
item = await attach.updateOne(item,
|
||||
compressedId: compressedAttachment.id);
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -309,8 +328,10 @@ class PostWriteController extends ChangeNotifier {
|
||||
return item;
|
||||
}
|
||||
|
||||
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
|
||||
Future<SnAttachment?> _tryCompressVideoCopy(
|
||||
BuildContext context, PostWriteMedia media) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
|
||||
return null;
|
||||
if (media.type != SnMediaType.video) return null;
|
||||
if (media.file == null) return null;
|
||||
if (VideoCompress.isCompressing) return null;
|
||||
@ -334,7 +355,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (!context.mounted) return null;
|
||||
|
||||
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
|
||||
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||
final compressedAttachment =
|
||||
await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||
|
||||
return compressedAttachment;
|
||||
}
|
||||
@ -370,18 +392,25 @@ class PostWriteController extends ChangeNotifier {
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments':
|
||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
||||
if (thumbnail != null && thumbnail!.attachment != null)
|
||||
'thumbnail': thumbnail!.attachment!.toJson(),
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.toJson())
|
||||
.toList(growable: true),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'categories':
|
||||
categories.map((ele) => {'alias': ele}).toList(growable: true),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedAt != null)
|
||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null)
|
||||
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
if (poll != null) 'poll': poll!.toJson(),
|
||||
@ -391,6 +420,12 @@ class PostWriteController extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
bool get isNotEmpty =>
|
||||
title.isNotEmpty ||
|
||||
description.isNotEmpty ||
|
||||
contentController.text.isNotEmpty ||
|
||||
attachments.isNotEmpty;
|
||||
|
||||
bool temporaryRestored = false;
|
||||
|
||||
void _temporaryLoad() {
|
||||
@ -403,18 +438,24 @@ class PostWriteController extends ChangeNotifier {
|
||||
titleController.text = data['title'] ?? '';
|
||||
descriptionController.text = data['description'] ?? '';
|
||||
rewardController.text = data['reward']?.toString() ?? '';
|
||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments
|
||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
||||
if (data['thumbnail'] != null)
|
||||
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||
attachments.addAll(data['attachments']
|
||||
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||
.cast<PostWriteMedia>());
|
||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||
visibility = data['visibility'];
|
||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
if (data['published_at'] != null)
|
||||
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||
if (data['published_until'] != null)
|
||||
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost =
|
||||
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost =
|
||||
data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
|
||||
temporaryRestored = true;
|
||||
@ -436,7 +477,10 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> sendPost(BuildContext context) async {
|
||||
Future<void> sendPost(
|
||||
BuildContext context, {
|
||||
bool saveAsDraft = false,
|
||||
}) async {
|
||||
if (isBusy || publisher == null) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@ -463,7 +507,9 @@ class PostWriteController extends ChangeNotifier {
|
||||
media.name,
|
||||
'interactive',
|
||||
null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||
mimetype: media.raw != null && media.type == SnMediaType.image
|
||||
? 'image/png'
|
||||
: null,
|
||||
);
|
||||
|
||||
var item = await attach.chunkedUploadParts(
|
||||
@ -472,16 +518,20 @@ class PostWriteController extends ChangeNotifier {
|
||||
place.$2,
|
||||
onProgress: (value) {
|
||||
// Calculate overall progress for attachments
|
||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||
progress = math.max(
|
||||
((i + value) / attachments.length) * kAttachmentProgressWeight,
|
||||
value);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (context.mounted) {
|
||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||
final compressedAttachment =
|
||||
await _tryCompressVideoCopy(context, media);
|
||||
if (compressedAttachment != null) {
|
||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||
item = await attach.updateOne(item,
|
||||
compressedId: compressedAttachment.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -508,7 +558,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
// Posting the content
|
||||
try {
|
||||
final baseProgressVal = progress!;
|
||||
await sn.client.request(
|
||||
final resp = await sn.client.request(
|
||||
[
|
||||
'/cgi/co/$mode',
|
||||
if (editingPost != null) '${editingPost!.id}',
|
||||
@ -518,36 +568,56 @@ class PostWriteController extends ChangeNotifier {
|
||||
'content': contentController.text,
|
||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||
if (descriptionController.text.isNotEmpty)
|
||||
'description': descriptionController.text,
|
||||
if (thumbnail != null && thumbnail!.attachment != null)
|
||||
'thumbnail': thumbnail!.attachment!.rid,
|
||||
'attachments': attachments
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
.toList(),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||
'visibility': visibility,
|
||||
'visible_users_list': visibleUsers,
|
||||
'invisible_users_list': invisibleUsers,
|
||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
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,
|
||||
if (reward != null) 'reward': reward,
|
||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
if (realm != null) 'realm': realm!.id,
|
||||
'is_draft': saveAsDraft,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
progress =
|
||||
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
onReceiveProgress: (count, total) {
|
||||
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
|
||||
progress = baseProgressVal +
|
||||
(kPostingProgressWeight / 2) +
|
||||
(count / total) * (kPostingProgressWeight / 2);
|
||||
notifyListeners();
|
||||
},
|
||||
options: Options(
|
||||
method: editingPost != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
reset();
|
||||
if (saveAsDraft) {
|
||||
if (!context.mounted) return;
|
||||
editingDraft = true;
|
||||
final out = SnPost.fromJson(resp.data);
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
editingPost = await pt.completePostData(out);
|
||||
notifyListeners();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -683,7 +753,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
repostingPost = null;
|
||||
mode = kTitleMap.keys.first;
|
||||
temporaryRestored = false;
|
||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
SharedPreferences.getInstance()
|
||||
.then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,6 @@ class $SnLocalChatChannelTable extends SnLocalChatChannel
|
||||
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
|
||||
'alias', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnChannel, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -60,7 +58,6 @@ class $SnLocalChatChannelTable extends SnLocalChatChannel
|
||||
} else if (isInserting) {
|
||||
context.missing(_aliasMeta);
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
@ -295,8 +292,6 @@ class $SnLocalChatMessageTable extends SnLocalChatMessage
|
||||
late final GeneratedColumn<int> senderId = GeneratedColumn<int>(
|
||||
'sender_id', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnChatMessage, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -338,7 +333,6 @@ class $SnLocalChatMessageTable extends SnLocalChatMessage
|
||||
context.handle(_senderIdMeta,
|
||||
senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta));
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
@ -604,8 +598,6 @@ class $SnLocalChannelMemberTable extends SnLocalChannelMember
|
||||
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
|
||||
'account_id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnChannelMember, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -655,7 +647,6 @@ class $SnLocalChannelMemberTable extends SnLocalChannelMember
|
||||
} else if (isInserting) {
|
||||
context.missing(_accountIdMeta);
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
@ -1265,8 +1256,6 @@ class $SnLocalAccountTable extends SnLocalAccount
|
||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||
'name', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnAccount, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -1308,7 +1297,6 @@ class $SnLocalAccountTable extends SnLocalAccount
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
@ -1582,8 +1570,6 @@ class $SnLocalAttachmentTable extends SnLocalAttachment
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnAttachment, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -1639,7 +1625,6 @@ class $SnLocalAttachmentTable extends SnLocalAttachment
|
||||
} else if (isInserting) {
|
||||
context.missing(_uuidMeta);
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('account_id')) {
|
||||
context.handle(_accountIdMeta,
|
||||
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
|
||||
@ -1968,8 +1953,6 @@ class $SnLocalStickerTable extends SnLocalSticker
|
||||
late final GeneratedColumn<String> fullAlias = GeneratedColumn<String>(
|
||||
'full_alias', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnSticker, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -2011,7 +1994,6 @@ class $SnLocalStickerTable extends SnLocalSticker
|
||||
} else if (isInserting) {
|
||||
context.missing(_fullAliasMeta);
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
@ -2261,8 +2243,6 @@ class $SnLocalStickerPackTable extends SnLocalStickerPack
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const VerificationMeta _contentMeta =
|
||||
const VerificationMeta('content');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnStickerPack, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
@ -2293,7 +2273,6 @@ class $SnLocalStickerPackTable extends SnLocalStickerPack
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
context.handle(_contentMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
|
@ -60,16 +60,24 @@ class SnPostContentProvider {
|
||||
|
||||
out[i] = out[i].copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
||||
thumbnail: attachments
|
||||
.where((ele) => ele?.rid == out[i].body['thumbnail'])
|
||||
.firstOrNull,
|
||||
attachments: attachments
|
||||
.where((ele) =>
|
||||
out[i].body['attachments']?.contains(ele?.rid) ?? false)
|
||||
.toList(),
|
||||
video: attachments
|
||||
.where((ele) => ele?.rid == out[i].body['video'])
|
||||
.firstOrNull,
|
||||
poll: poll,
|
||||
realm: realm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
uids.addAll(
|
||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
await _ud.listAccount(uids);
|
||||
|
||||
return out;
|
||||
@ -107,15 +115,23 @@ class SnPostContentProvider {
|
||||
|
||||
out = out.copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
||||
thumbnail: attachments
|
||||
.where((ele) => ele?.rid == out.body['thumbnail'])
|
||||
.firstOrNull,
|
||||
attachments: attachments
|
||||
.where(
|
||||
(ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
|
||||
.toList(),
|
||||
video: attachments
|
||||
.where((ele) => ele?.rid == out.body['video'])
|
||||
.firstOrNull,
|
||||
poll: poll,
|
||||
realm: realm,
|
||||
),
|
||||
);
|
||||
|
||||
uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
uids.addAll(
|
||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
await _ud.listAccount(uids);
|
||||
|
||||
return out;
|
||||
@ -138,17 +154,25 @@ class SnPostContentProvider {
|
||||
Iterable<String>? tags,
|
||||
String? realm,
|
||||
String? channel,
|
||||
bool isDraft = false,
|
||||
bool isShuffle = false,
|
||||
}) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
if (type != null) 'type': type,
|
||||
if (author != null) 'author': author,
|
||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||
if (realm != null) 'realm': realm,
|
||||
if (channel != null) 'channel': channel,
|
||||
});
|
||||
final resp = await _sn.client.get(
|
||||
isShuffle
|
||||
? '/cgi/co/recommendations/shuffle'
|
||||
: '/cgi/co/posts${isDraft ? '/drafts' : ''}',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
if (type != null) 'type': type,
|
||||
if (author != null) 'author': author,
|
||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||
if (categories?.isNotEmpty ?? false)
|
||||
'categories': categories!.join(','),
|
||||
if (realm != null) 'realm': realm,
|
||||
if (channel != null) 'channel': channel,
|
||||
},
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
);
|
||||
@ -161,7 +185,8 @@ class SnPostContentProvider {
|
||||
int take = 10,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts/$parentId/replies', queryParameters: {
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/co/posts/$parentId/replies', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
});
|
||||
@ -200,4 +225,9 @@ class SnPostContentProvider {
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<SnPost> completePostData(SnPost post) async {
|
||||
final out = await _preloadRelatedDataSingle(post);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ class UserDirectoryProvider {
|
||||
|
||||
final Map<String, int> _idCache = {};
|
||||
final Map<int, SnAccount> _cache = {};
|
||||
DateTime? _cacheExpiredAt;
|
||||
|
||||
Future<int> loadAccountCache({int max = 100}) async {
|
||||
final out = await (_dt.db.snLocalAccount.select()..limit(max)).get();
|
||||
@ -26,11 +27,18 @@ class UserDirectoryProvider {
|
||||
_cache[ele.id] = ele.content;
|
||||
_idCache[ele.name] = ele.id;
|
||||
}
|
||||
_cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
|
||||
return out.length;
|
||||
}
|
||||
|
||||
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
|
||||
// In-memory cache
|
||||
if (_cacheExpiredAt != null && _cacheExpiredAt!.isAfter(DateTime.now())) {
|
||||
_cache.clear();
|
||||
_cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
|
||||
} else {
|
||||
_cacheExpiredAt ??= DateTime.now().add(const Duration(hours: 1));
|
||||
}
|
||||
final out = List<SnAccount?>.generate(id.length, (e) => null);
|
||||
final plannedQuery = <int>{};
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
@ -62,6 +70,7 @@ class UserDirectoryProvider {
|
||||
plannedQuery.remove(dbResp[idx].id);
|
||||
}
|
||||
// Remote server
|
||||
_saveToLocal(out.where((ele) => ele != null).cast());
|
||||
if (plannedQuery.isEmpty) return out;
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
||||
|
@ -28,7 +28,9 @@ import 'package:surface/screens/news/news_detail.dart';
|
||||
import 'package:surface/screens/news/news_list.dart';
|
||||
import 'package:surface/screens/notification.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/screens/post/post_draft.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/screens/post/post_shuffle.dart';
|
||||
import 'package:surface/screens/post/publisher_page.dart';
|
||||
import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
@ -66,10 +68,15 @@ final _appRoutes = [
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/write/:mode',
|
||||
path: '/draft',
|
||||
name: 'postDraftBox',
|
||||
builder: (context, state) => const PostDraftBox(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/write',
|
||||
name: 'postEditor',
|
||||
builder: (context, state) => PostEditorScreen(
|
||||
mode: state.pathParameters['mode']!,
|
||||
mode: state.uri.queryParameters['mode'],
|
||||
postEditId: int.tryParse(
|
||||
state.uri.queryParameters['editing'] ?? '',
|
||||
),
|
||||
@ -82,6 +89,11 @@ final _appRoutes = [
|
||||
extraProps: state.extra as PostEditorExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/shuffle',
|
||||
name: 'postShuffle',
|
||||
builder: (context, state) => const PostShuffleScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/search',
|
||||
name: 'postSearch',
|
||||
|
@ -11,7 +11,9 @@ import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_status.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -112,7 +114,14 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(content: ua.user!.avatar, radius: 28),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountImage(content: ua.user!.avatar, radius: 28),
|
||||
_AccountStatusWidget(account: ua.user!),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
@ -290,3 +299,81 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountStatusWidget extends StatefulWidget {
|
||||
final SnAccount account;
|
||||
const _AccountStatusWidget({required this.account});
|
||||
|
||||
@override
|
||||
State<_AccountStatusWidget> createState() => _AccountStatusWidgetState();
|
||||
}
|
||||
|
||||
class _AccountStatusWidgetState extends State<_AccountStatusWidget> {
|
||||
SnAccountStatusInfo? _status;
|
||||
|
||||
Future<void> _fetchStatus() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp =
|
||||
await sn.client.get('/cgi/id/users/${widget.account.name}/status');
|
||||
setState(() {
|
||||
_status = SnAccountStatusInfo.fromJson(resp.data);
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_status != null
|
||||
? (_status!.status?.label.isNotEmpty ?? false)
|
||||
? _status!.status!.label
|
||||
: _status!.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
const Gap(4),
|
||||
Icon(
|
||||
(_status?.isDisturbable ?? true)
|
||||
? Symbols.circle
|
||||
: Symbols.do_not_disturb_on,
|
||||
fill: (_status?.isOnline ?? false) ? 1 : 0,
|
||||
size: 16,
|
||||
color: (_status?.isOnline ?? false)
|
||||
? (_status?.isDisturbable ?? true)
|
||||
? Colors.green
|
||||
: Colors.red
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => AccountStatusActionPopup(
|
||||
currentStatus: _status,
|
||||
),
|
||||
).then((value) {
|
||||
if (value == true && mounted) {
|
||||
_fetchStatus();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:surface/theme.dart';
|
||||
@ -450,19 +451,25 @@ class _UserScreenState extends State<UserScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
(_status?.isDisturbable ?? true)
|
||||
? Symbols.circle
|
||||
: Symbols.do_not_disturb_on,
|
||||
fill: (_status?.isOnline ?? false) ? 1 : 0,
|
||||
size: 16,
|
||||
color: (_status?.isOnline ?? false)
|
||||
? Colors.green
|
||||
? (_status?.isDisturbable ?? true)
|
||||
? Colors.green
|
||||
: Colors.red
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
_status != null
|
||||
? _status!.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
? (_status!.status?.label.isNotEmpty ?? false)
|
||||
? _status!.status!.label
|
||||
: _status!.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (_status != null &&
|
||||
@ -484,34 +491,7 @@ class _UserScreenState extends State<UserScreen>
|
||||
Wrap(
|
||||
children: _account!.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
||||
'unknown'.tr(),
|
||||
),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: ele.metadata['color'] != null
|
||||
? HexColor.fromHex(ele.metadata['color']!)
|
||||
: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
|
@ -204,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||
child: const Icon(Symbols.close, size: 28),
|
||||
@ -213,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -19,6 +18,9 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
const kPostChannels = ['Global', 'Friends', 'Following'];
|
||||
const kPostChannelIcons = [Symbols.globe, Symbols.group, Symbols.subscriptions];
|
||||
|
||||
const Map<String, IconData> kCategoryIcons = {
|
||||
'technology': Symbols.tools_wrench,
|
||||
'gaming': Symbols.gamepad,
|
||||
@ -39,17 +41,17 @@ class ExploreScreen extends StatefulWidget {
|
||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||
}
|
||||
|
||||
// You know what? I'm not going to make this a global variable.
|
||||
// Cuz the global key make the selected category not update to child widget when the category is changed.
|
||||
SnPostCategory? _selectedCategory;
|
||||
|
||||
class _ExploreScreenState extends State<ExploreScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController =
|
||||
TabController(length: 4, vsync: this);
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController = TabController(
|
||||
length: kPostChannels.length,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
final _fabKey = GlobalKey<ExpandableFabState>();
|
||||
final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>());
|
||||
final _listKey = GlobalKey<_PostListWidgetState>();
|
||||
|
||||
bool _showCategories = false;
|
||||
|
||||
final List<SnPostCategory> _categories = List.empty(growable: true);
|
||||
|
||||
@ -69,14 +71,68 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _clearFilter() {
|
||||
_selectedCategory = null;
|
||||
final List<SnRealm> _realms = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
final rels = context.read<SnRealmProvider>();
|
||||
final out = await rels.listAvailableRealms();
|
||||
setState(() {
|
||||
_realms.addAll(out);
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleShowCategories() {
|
||||
_showCategories = !_showCategories;
|
||||
if (_showCategories) {
|
||||
_tabController = TabController(length: _categories.length, vsync: this);
|
||||
_listKey.currentState?.setCategory(_categories[_tabController.index]);
|
||||
_listKey.currentState?.refreshPosts();
|
||||
} else {
|
||||
_tabController = TabController(length: kPostChannels.length, vsync: this);
|
||||
_listKey.currentState?.setCategory(null);
|
||||
_listKey.currentState?.refreshPosts();
|
||||
}
|
||||
_tabListen();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _tabListen() {
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.indexIsChanging) {
|
||||
if (_showCategories) {
|
||||
_listKey.currentState?.setCategory(_categories[_tabController.index]);
|
||||
_listKey.currentState?.refreshPosts();
|
||||
return;
|
||||
}
|
||||
switch (_tabController.index) {
|
||||
case 0:
|
||||
case 3:
|
||||
_listKey.currentState?.setChannel(null);
|
||||
break;
|
||||
case 1:
|
||||
_listKey.currentState?.setChannel('friends');
|
||||
break;
|
||||
case 2:
|
||||
_listKey.currentState?.setChannel('following');
|
||||
break;
|
||||
}
|
||||
_listKey.currentState?.refreshPosts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchCategories();
|
||||
super.initState();
|
||||
_tabListen();
|
||||
_fetchCategories();
|
||||
_fetchRealms();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -86,7 +142,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
}
|
||||
|
||||
Future<void> refreshPosts() async {
|
||||
await _listKeys[_tabController.index].currentState?.refreshPosts();
|
||||
await _listKey.currentState?.refreshPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -111,7 +167,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||
child: const Icon(Symbols.close, size: 28),
|
||||
@ -120,90 +175,39 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeStory').tr(),
|
||||
Text('writePost').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeStory'.tr(),
|
||||
tooltip: 'writePost'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'stories',
|
||||
}).then((value) {
|
||||
GoRouter.of(context).pushNamed('postEditor').then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.post_rounded),
|
||||
child: const Icon(Symbols.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeArticle').tr(),
|
||||
Text('postDraftBox').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeArticle'.tr(),
|
||||
tooltip: 'postDraftBox'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'articles',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
GoRouter.of(context).pushNamed('postDraftBox');
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.news),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeQuestion').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeQuestion'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'questions',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.question_answer),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('writePostTypeVideo').tr(),
|
||||
const Gap(20),
|
||||
FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: 'writePostTypeVideo'.tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
||||
'mode': 'videos',
|
||||
}).then((value) {
|
||||
if (value == true) {
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_fabKey.currentState!.toggle();
|
||||
},
|
||||
child: const Icon(Symbols.video_call),
|
||||
child: const Icon(Symbols.box_edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -216,25 +220,74 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenExplore').tr(),
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.shuffle),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: _listKey.currentState?.realm != null
|
||||
? AccountImage(
|
||||
content: _listKey.currentState!.realm!.avatar,
|
||||
radius: 14,
|
||||
)
|
||||
: Image.asset(
|
||||
'assets/icon/icon-dark.png',
|
||||
width: 32,
|
||||
height: 32,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _PostListRealmPopup(
|
||||
realms: _realms,
|
||||
onUpdate: (realm) {
|
||||
_listKey.currentState?.setRealm(realm);
|
||||
_listKey.currentState?.refreshPosts();
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floating: true,
|
||||
snap: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.category),
|
||||
style: _showCategories
|
||||
? ButtonStyle(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _PostCategoryPickerPopup(
|
||||
categories: _categories,
|
||||
selected: _selectedCategory,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null && context.mounted) {
|
||||
_selectedCategory = value == false ? null : value;
|
||||
refreshPosts();
|
||||
}
|
||||
});
|
||||
_toggleShowCategories();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@ -246,122 +299,79 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
isScrollable: _showCategories,
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Symbols.globe,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannelGlobal',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
tabs: _showCategories
|
||||
? [
|
||||
for (final category in _categories)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kCategoryIcons[category.alias] ??
|
||||
Symbols.question_mark,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postCategory${category.alias.capitalize()}'
|
||||
.trExists()
|
||||
? 'postCategory${category.alias.capitalize()}'
|
||||
.tr()
|
||||
: category.name,
|
||||
maxLines: 1,
|
||||
).textColor(
|
||||
Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
for (final channel in kPostChannels)
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
kPostChannelIcons[
|
||||
kPostChannels.indexOf(channel)],
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannel$channel',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Symbols.group,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannelFriends',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
).tr().textColor(
|
||||
Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Symbols.subscriptions,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannelFollowing',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Symbols.workspaces,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postChannelRealm',
|
||||
maxLines: 1,
|
||||
).tr().textColor(
|
||||
Theme.of(context).appBarTheme.foregroundColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_PostListWidget(
|
||||
key: _listKeys[0],
|
||||
onClearFilter: _clearFilter,
|
||||
),
|
||||
_PostListWidget(
|
||||
key: _listKeys[1],
|
||||
channel: 'friends',
|
||||
onClearFilter: _clearFilter,
|
||||
),
|
||||
_PostListWidget(
|
||||
key: _listKeys[2],
|
||||
channel: 'following',
|
||||
onClearFilter: _clearFilter,
|
||||
),
|
||||
_PostListWidget(
|
||||
key: _listKeys[3],
|
||||
withRealm: true,
|
||||
onClearFilter: _clearFilter,
|
||||
),
|
||||
],
|
||||
body: _PostListWidget(
|
||||
key: _listKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -369,15 +379,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
}
|
||||
|
||||
class _PostListWidget extends StatefulWidget {
|
||||
final String? channel;
|
||||
final bool withRealm;
|
||||
final Function onClearFilter;
|
||||
|
||||
const _PostListWidget(
|
||||
{super.key,
|
||||
this.channel,
|
||||
this.withRealm = false,
|
||||
required this.onClearFilter});
|
||||
const _PostListWidget({super.key});
|
||||
|
||||
@override
|
||||
State<_PostListWidget> createState() => _PostListWidgetState();
|
||||
@ -386,25 +388,13 @@ class _PostListWidget extends StatefulWidget {
|
||||
class _PostListWidgetState extends State<_PostListWidget> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
final List<SnRealm> _realms = List.empty(growable: true);
|
||||
SnRealm? _selectedRealm;
|
||||
int? _postCount;
|
||||
SnRealm? get realm => _selectedRealm;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
final rels = context.read<SnRealmProvider>();
|
||||
final out = await rels.listAvailableRealms();
|
||||
setState(() {
|
||||
_realms.addAll(out);
|
||||
_selectedRealm = out.firstOrNull;
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
SnRealm? _selectedRealm;
|
||||
String? _selectedChannel;
|
||||
SnPostCategory? _selectedCategory;
|
||||
int? _postCount;
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||
@ -416,7 +406,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
|
||||
channel: widget.channel,
|
||||
channel: _selectedChannel,
|
||||
realm: _selectedRealm?.alias,
|
||||
);
|
||||
final out = result.$1;
|
||||
@ -429,6 +419,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void setChannel(String? channel) {
|
||||
_selectedChannel = channel;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setRealm(SnRealm? realm) {
|
||||
_selectedRealm = realm;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setCategory(SnPostCategory? category) {
|
||||
_selectedCategory = category;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> refreshPosts() {
|
||||
_postCount = null;
|
||||
_posts.clear();
|
||||
@ -438,13 +443,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.withRealm) {
|
||||
_fetchRealms().then((_) {
|
||||
_fetchPosts();
|
||||
});
|
||||
} else {
|
||||
_fetchPosts();
|
||||
}
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -467,52 +466,13 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.clear),
|
||||
onPressed: () {
|
||||
widget.onClearFilter.call();
|
||||
setState(() => _selectedCategory = null);
|
||||
refreshPosts();
|
||||
},
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.only(left: 20, right: 4),
|
||||
),
|
||||
if (widget.withRealm)
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnRealm>(
|
||||
isExpanded: true,
|
||||
items: _realms
|
||||
.map(
|
||||
(ele) => DropdownMenuItem<SnRealm>(
|
||||
value: ele,
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(
|
||||
content: ele.avatar,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 16),
|
||||
radius: 14,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
ele.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: _selectedRealm,
|
||||
onChanged: (SnRealm? value) {
|
||||
setState(() => _selectedRealm = value);
|
||||
refreshPosts();
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(left: 4, right: 12),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.withRealm) const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
@ -521,6 +481,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||
onRefresh: () => refreshPosts(),
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
centerLoading: true,
|
||||
@ -542,18 +503,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
),
|
||||
).padding(top: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostCategoryPickerPopup extends StatelessWidget {
|
||||
final List<SnPostCategory> categories;
|
||||
final SnPostCategory? selected;
|
||||
class _PostListRealmPopup extends StatelessWidget {
|
||||
final List<SnRealm>? realms;
|
||||
final Function(SnRealm?) onUpdate;
|
||||
|
||||
const _PostCategoryPickerPopup({required this.categories, this.selected});
|
||||
const _PostListRealmPopup({
|
||||
required this.realms,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -563,62 +527,38 @@ class _PostCategoryPickerPopup extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.category, size: 24),
|
||||
const Icon(Symbols.face, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCategory')
|
||||
.tr()
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.clear),
|
||||
title: Text('postFilterReset').tr(),
|
||||
subtitle: Text('postFilterResetDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
leading: const Icon(Symbols.close),
|
||||
title: Text('postInGlobal').tr(),
|
||||
subtitle: Text('postViewInGlobalDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
Navigator.pop(context, false);
|
||||
onUpdate.call(null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 1,
|
||||
children: categories
|
||||
.map(
|
||||
(ele) => InkWell(
|
||||
onTap: () {
|
||||
_selectedCategory = ele;
|
||||
Navigator.pop(context, ele);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
kCategoryIcons[ele.alias] ?? Symbols.question_mark,
|
||||
color: selected == ele
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postCategory${ele.alias.capitalize()}'.trExists()
|
||||
? 'postCategory${ele.alias.capitalize()}'.tr()
|
||||
: ele.name,
|
||||
)
|
||||
.textStyle(Theme.of(context).textTheme.titleMedium!)
|
||||
.textColor(selected == ele
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: ListView.builder(
|
||||
itemCount: realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = realms![idx];
|
||||
return ListTile(
|
||||
title: Text(realm.name),
|
||||
subtitle: Text('@${realm.alias}'),
|
||||
leading: AccountImage(content: realm.avatar, radius: 18),
|
||||
onTap: () {
|
||||
onUpdate.call(realm);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -546,11 +546,26 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
'+${_todayRecord!.resultExperience} EXP',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_todayRecord!.resultCoin >= 0)
|
||||
if (_todayRecord!.resultCoin > 0)
|
||||
Text(
|
||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
)
|
||||
),
|
||||
if (_todayRecord!.currentStreak > 0)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.local_fire_department,
|
||||
size: 14,
|
||||
).padding(bottom: 2),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'checkInStreak'
|
||||
.plural(_todayRecord!.currentStreak),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
).padding(top: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
88
lib/screens/post/post_draft.dart
Normal file
88
lib/screens/post/post_draft.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class PostDraftBox extends StatefulWidget {
|
||||
const PostDraftBox({super.key});
|
||||
|
||||
@override
|
||||
State<PostDraftBox> createState() => _PostDraftBoxState();
|
||||
}
|
||||
|
||||
class _PostDraftBoxState extends State<PostDraftBox> {
|
||||
bool _isBusy = false;
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
int? _totalCount;
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final resp = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
isDraft: true,
|
||||
);
|
||||
final out = resp.$1;
|
||||
_totalCount = resp.$2;
|
||||
if (!mounted) return;
|
||||
_posts.addAll(out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('postDraftBox').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_posts.clear();
|
||||
return _fetchPosts();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
hasReachedMax:
|
||||
_totalCount != null && _posts.length >= _totalCount!,
|
||||
itemCount: _posts.length,
|
||||
onFetchData: () => _fetchPosts(),
|
||||
itemBuilder: (context, idx) {
|
||||
final ele = _posts[idx];
|
||||
return OpenablePostItem(
|
||||
data: ele,
|
||||
onChanged: (data) {
|
||||
_posts[idx] = data;
|
||||
},
|
||||
onDeleted: () {
|
||||
_posts.clear();
|
||||
_fetchPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -36,7 +37,8 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/widgets/post/post_poll_editor.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../providers/sn_realm.dart';
|
||||
const kPostTypes = ['Story', 'Article', 'Question', 'Video'];
|
||||
const kPostTypeAliases = ['stories', 'articles', 'questions', 'videos'];
|
||||
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
@ -53,7 +55,7 @@ class PostEditorExtra {
|
||||
}
|
||||
|
||||
class PostEditorScreen extends StatefulWidget {
|
||||
final String mode;
|
||||
final String? mode;
|
||||
final int? postEditId;
|
||||
final int? postReplyId;
|
||||
final int? postRepostId;
|
||||
@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget {
|
||||
State<PostEditorScreen> createState() => _PostEditorScreenState();
|
||||
}
|
||||
|
||||
class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController =
|
||||
TabController(length: 4, vsync: this);
|
||||
late final PostWriteController _writeController = PostWriteController(
|
||||
doLoadFromTemporary: widget.postEditId == null,
|
||||
);
|
||||
@ -133,6 +138,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
final HotKey _saveDraftHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyS,
|
||||
modifiers: [
|
||||
(!kIsWeb && Platform.isMacOS)
|
||||
? HotKeyModifier.meta
|
||||
: HotKeyModifier.control
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
void _registerHotKey() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
@ -148,6 +162,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
]);
|
||||
setState(() {});
|
||||
});
|
||||
hotKeyManager.register(_saveDraftHotKey, keyDownHandler: (_) async {
|
||||
if (mounted) {
|
||||
_writeController.sendPost(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showPublisherPopup() {
|
||||
@ -209,9 +228,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_writeController.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
hotKeyManager.unregister(_saveDraftHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@ -220,14 +241,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerHotKey();
|
||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
||||
context.showErrorDialog('Unknown post type');
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
_writeController.setMode(widget.mode);
|
||||
}
|
||||
_fetchRealms();
|
||||
_fetchPublishers();
|
||||
if (widget.mode != null) {
|
||||
_writeController.setMode(widget.mode!);
|
||||
}
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.indexIsChanging) {
|
||||
_writeController.setMode(kPostTypeAliases[_tabController.index]);
|
||||
}
|
||||
});
|
||||
_writeController.fetchRelatedPost(
|
||||
context,
|
||||
editing: widget.postEditId,
|
||||
@ -255,38 +278,55 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _writeController.title.isNotEmpty
|
||||
? _writeController.title
|
||||
: 'untitled'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
title: Text(
|
||||
_writeController.title.isNotEmpty
|
||||
? _writeController.title
|
||||
: 'untitled'.tr(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: _writeController.editingDraft
|
||||
? const Icon(Icons.save)
|
||||
: const Icon(Symbols.save_as),
|
||||
onPressed: () {
|
||||
_writeController.sendPost(context, saveAsDraft: true).then(
|
||||
(_) {
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('postDraftSaved'.tr());
|
||||
HapticFeedback.mediumImpact();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.tune),
|
||||
onPressed: _writeController.isBusy ? null : _updateMeta,
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: _writeController.isNotEmpty || widget.mode != null
|
||||
? null
|
||||
: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
for (final type in kPostTypes)
|
||||
Tab(
|
||||
child: Text(
|
||||
'postType$type'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.appBarTheme
|
||||
.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (_writeController.editingPost != null)
|
||||
if (_writeController.editingPost != null &&
|
||||
!_writeController.editingDraft)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 20, right: 20),
|
||||
@ -374,7 +414,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: StyledWidget(switch (_writeController.mode) {
|
||||
child: switch (_writeController.mode) {
|
||||
'stories' => _PostStoryEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
@ -396,8 +436,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
onTapRealm: _showRealmPopup,
|
||||
),
|
||||
_ => const Placeholder(),
|
||||
})
|
||||
.padding(top: 8),
|
||||
},
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty ||
|
||||
_writeController.thumbnail != null)
|
||||
@ -720,7 +759,7 @@ class _PostStoryEditor extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -969,7 +1008,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -1053,7 +1092,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(top: 8),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1154,7 +1193,7 @@ class _PostVideoEditor extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
132
lib/screens/post/post_shuffle.dart
Normal file
132
lib/screens/post/post_shuffle.dart
Normal file
@ -0,0 +1,132 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
|
||||
class PostShuffleScreen extends StatefulWidget {
|
||||
const PostShuffleScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PostShuffleScreen> createState() => _PostShuffleScreenState();
|
||||
}
|
||||
|
||||
class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
||||
late final CardSwiperController _cardController = CardSwiperController();
|
||||
|
||||
bool _isBusy = false;
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
_posts.clear();
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
isShuffle: true,
|
||||
);
|
||||
_posts.addAll(result.$1);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_cardController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('postShuffle').tr(),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (_isBusy || _posts.isEmpty)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: CardSwiper(
|
||||
controller: _cardController,
|
||||
isLoop: false,
|
||||
padding: EdgeInsets.zero,
|
||||
cardsCount: _posts.length,
|
||||
cardBuilder: (context, idx, _, __) {
|
||||
final ele = _posts[idx];
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: OpenablePostItem(
|
||||
key: ValueKey(ele),
|
||||
data: ele,
|
||||
maxWidth: 640,
|
||||
onChanged: (ele) {
|
||||
_posts[idx] = ele;
|
||||
setState(() {});
|
||||
},
|
||||
onDeleted: () {
|
||||
_fetchPosts();
|
||||
},
|
||||
).padding(
|
||||
all: 24,
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom + 16 + 50,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onEnd: () {
|
||||
_fetchPosts();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_isBusy && _posts.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton.filled(
|
||||
icon: const Icon(Symbols.next_plan),
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () {
|
||||
_cardController.swipe(CardSwiperDirection.right);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -61,7 +61,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {
|
||||
queryParameters: {
|
||||
'mode': 'stories',
|
||||
},
|
||||
extra: PostEditorExtra(
|
||||
|
@ -119,13 +119,33 @@ abstract class SnAccountStatusInfo with _$SnAccountStatusInfo {
|
||||
required bool isDisturbable,
|
||||
required bool isOnline,
|
||||
required DateTime? lastSeenAt,
|
||||
required dynamic status,
|
||||
required SnAccountStatus? status,
|
||||
}) = _SnAccountStatusInfo;
|
||||
|
||||
factory SnAccountStatusInfo.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAccountStatusInfoFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnAccountStatus with _$SnAccountStatus {
|
||||
const factory SnAccountStatus({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String type,
|
||||
required String label,
|
||||
required int attitude,
|
||||
required bool isNoDisturb,
|
||||
required bool isInvisible,
|
||||
required DateTime? clearAt,
|
||||
required int accountId,
|
||||
}) = _SnAccountStatus;
|
||||
|
||||
factory SnAccountStatus.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAccountStatusFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnAbuseReport with _$SnAbuseReport {
|
||||
const factory SnAbuseReport({
|
||||
|
@ -2139,7 +2139,7 @@ mixin _$SnAccountStatusInfo {
|
||||
bool get isDisturbable;
|
||||
bool get isOnline;
|
||||
DateTime? get lastSeenAt;
|
||||
dynamic get status;
|
||||
SnAccountStatus? get status;
|
||||
|
||||
/// Create a copy of SnAccountStatusInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2163,13 +2163,13 @@ mixin _$SnAccountStatusInfo {
|
||||
other.isOnline == isOnline) &&
|
||||
(identical(other.lastSeenAt, lastSeenAt) ||
|
||||
other.lastSeenAt == lastSeenAt) &&
|
||||
const DeepCollectionEquality().equals(other.status, status));
|
||||
(identical(other.status, status) || other.status == status));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
|
||||
lastSeenAt, const DeepCollectionEquality().hash(status));
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -2187,7 +2187,9 @@ abstract mixin class $SnAccountStatusInfoCopyWith<$Res> {
|
||||
{bool isDisturbable,
|
||||
bool isOnline,
|
||||
DateTime? lastSeenAt,
|
||||
dynamic status});
|
||||
SnAccountStatus? status});
|
||||
|
||||
$SnAccountStatusCopyWith<$Res>? get status;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2224,9 +2226,23 @@ class _$SnAccountStatusInfoCopyWithImpl<$Res>
|
||||
status: freezed == status
|
||||
? _self.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnAccountStatus?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnAccountStatusInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountStatusCopyWith<$Res>? get status {
|
||||
if (_self.status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountStatusCopyWith<$Res>(_self.status!, (value) {
|
||||
return _then(_self.copyWith(status: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2247,7 +2263,7 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
|
||||
@override
|
||||
final DateTime? lastSeenAt;
|
||||
@override
|
||||
final dynamic status;
|
||||
final SnAccountStatus? status;
|
||||
|
||||
/// Create a copy of SnAccountStatusInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2276,13 +2292,13 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
|
||||
other.isOnline == isOnline) &&
|
||||
(identical(other.lastSeenAt, lastSeenAt) ||
|
||||
other.lastSeenAt == lastSeenAt) &&
|
||||
const DeepCollectionEquality().equals(other.status, status));
|
||||
(identical(other.status, status) || other.status == status));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
|
||||
lastSeenAt, const DeepCollectionEquality().hash(status));
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -2302,7 +2318,10 @@ abstract mixin class _$SnAccountStatusInfoCopyWith<$Res>
|
||||
{bool isDisturbable,
|
||||
bool isOnline,
|
||||
DateTime? lastSeenAt,
|
||||
dynamic status});
|
||||
SnAccountStatus? status});
|
||||
|
||||
@override
|
||||
$SnAccountStatusCopyWith<$Res>? get status;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2339,7 +2358,386 @@ class __$SnAccountStatusInfoCopyWithImpl<$Res>
|
||||
status: freezed == status
|
||||
? _self.status
|
||||
: status // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
as SnAccountStatus?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnAccountStatusInfo
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountStatusCopyWith<$Res>? get status {
|
||||
if (_self.status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountStatusCopyWith<$Res>(_self.status!, (value) {
|
||||
return _then(_self.copyWith(status: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnAccountStatus {
|
||||
int get id;
|
||||
DateTime get createdAt;
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get type;
|
||||
String get label;
|
||||
int get attitude;
|
||||
bool get isNoDisturb;
|
||||
bool get isInvisible;
|
||||
DateTime? get clearAt;
|
||||
int get accountId;
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountStatusCopyWith<SnAccountStatus> get copyWith =>
|
||||
_$SnAccountStatusCopyWithImpl<SnAccountStatus>(
|
||||
this as SnAccountStatus, _$identity);
|
||||
|
||||
/// Serializes this SnAccountStatus to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is SnAccountStatus &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.attitude, attitude) ||
|
||||
other.attitude == attitude) &&
|
||||
(identical(other.isNoDisturb, isNoDisturb) ||
|
||||
other.isNoDisturb == isNoDisturb) &&
|
||||
(identical(other.isInvisible, isInvisible) ||
|
||||
other.isInvisible == isInvisible) &&
|
||||
(identical(other.clearAt, clearAt) || other.clearAt == clearAt) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
type,
|
||||
label,
|
||||
attitude,
|
||||
isNoDisturb,
|
||||
isInvisible,
|
||||
clearAt,
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, label: $label, attitude: $attitude, isNoDisturb: $isNoDisturb, isInvisible: $isInvisible, clearAt: $clearAt, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnAccountStatusCopyWith<$Res> {
|
||||
factory $SnAccountStatusCopyWith(
|
||||
SnAccountStatus value, $Res Function(SnAccountStatus) _then) =
|
||||
_$SnAccountStatusCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String type,
|
||||
String label,
|
||||
int attitude,
|
||||
bool isNoDisturb,
|
||||
bool isInvisible,
|
||||
DateTime? clearAt,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnAccountStatusCopyWithImpl<$Res>
|
||||
implements $SnAccountStatusCopyWith<$Res> {
|
||||
_$SnAccountStatusCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnAccountStatus _self;
|
||||
final $Res Function(SnAccountStatus) _then;
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? type = null,
|
||||
Object? label = null,
|
||||
Object? attitude = null,
|
||||
Object? isNoDisturb = null,
|
||||
Object? isInvisible = null,
|
||||
Object? clearAt = freezed,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _self.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
attitude: null == attitude
|
||||
? _self.attitude
|
||||
: attitude // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isNoDisturb: null == isNoDisturb
|
||||
? _self.isNoDisturb
|
||||
: isNoDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isInvisible: null == isInvisible
|
||||
? _self.isInvisible
|
||||
: isInvisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
clearAt: freezed == clearAt
|
||||
? _self.clearAt
|
||||
: clearAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _SnAccountStatus implements SnAccountStatus {
|
||||
const _SnAccountStatus(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.type,
|
||||
required this.label,
|
||||
required this.attitude,
|
||||
required this.isNoDisturb,
|
||||
required this.isInvisible,
|
||||
required this.clearAt,
|
||||
required this.accountId});
|
||||
factory _SnAccountStatus.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnAccountStatusFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final DateTime? deletedAt;
|
||||
@override
|
||||
final String type;
|
||||
@override
|
||||
final String label;
|
||||
@override
|
||||
final int attitude;
|
||||
@override
|
||||
final bool isNoDisturb;
|
||||
@override
|
||||
final bool isInvisible;
|
||||
@override
|
||||
final DateTime? clearAt;
|
||||
@override
|
||||
final int accountId;
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnAccountStatusCopyWith<_SnAccountStatus> get copyWith =>
|
||||
__$SnAccountStatusCopyWithImpl<_SnAccountStatus>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnAccountStatusToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _SnAccountStatus &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.type, type) || other.type == type) &&
|
||||
(identical(other.label, label) || other.label == label) &&
|
||||
(identical(other.attitude, attitude) ||
|
||||
other.attitude == attitude) &&
|
||||
(identical(other.isNoDisturb, isNoDisturb) ||
|
||||
other.isNoDisturb == isNoDisturb) &&
|
||||
(identical(other.isInvisible, isInvisible) ||
|
||||
other.isInvisible == isInvisible) &&
|
||||
(identical(other.clearAt, clearAt) || other.clearAt == clearAt) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
type,
|
||||
label,
|
||||
attitude,
|
||||
isNoDisturb,
|
||||
isInvisible,
|
||||
clearAt,
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, label: $label, attitude: $attitude, isNoDisturb: $isNoDisturb, isInvisible: $isInvisible, clearAt: $clearAt, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnAccountStatusCopyWith<$Res>
|
||||
implements $SnAccountStatusCopyWith<$Res> {
|
||||
factory _$SnAccountStatusCopyWith(
|
||||
_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) =
|
||||
__$SnAccountStatusCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
String type,
|
||||
String label,
|
||||
int attitude,
|
||||
bool isNoDisturb,
|
||||
bool isInvisible,
|
||||
DateTime? clearAt,
|
||||
int accountId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$SnAccountStatusCopyWithImpl<$Res>
|
||||
implements _$SnAccountStatusCopyWith<$Res> {
|
||||
__$SnAccountStatusCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnAccountStatus _self;
|
||||
final $Res Function(_SnAccountStatus) _then;
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? type = null,
|
||||
Object? label = null,
|
||||
Object? attitude = null,
|
||||
Object? isNoDisturb = null,
|
||||
Object? isInvisible = null,
|
||||
Object? clearAt = freezed,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
return _then(_SnAccountStatus(
|
||||
id: null == id
|
||||
? _self.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _self.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _self.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _self.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
type: null == type
|
||||
? _self.type
|
||||
: type // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
label: null == label
|
||||
? _self.label
|
||||
: label // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
attitude: null == attitude
|
||||
? _self.attitude
|
||||
: attitude // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isNoDisturb: null == isNoDisturb
|
||||
? _self.isNoDisturb
|
||||
: isNoDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isInvisible: null == isInvisible
|
||||
? _self.isInvisible
|
||||
: isInvisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
clearAt: freezed == clearAt
|
||||
? _self.clearAt
|
||||
: clearAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +210,9 @@ _SnAccountStatusInfo _$SnAccountStatusInfoFromJson(Map<String, dynamic> json) =>
|
||||
lastSeenAt: json['last_seen_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_seen_at'] as String),
|
||||
status: json['status'],
|
||||
status: json['status'] == null
|
||||
? null
|
||||
: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnAccountStatusInfoToJson(
|
||||
@ -219,7 +221,41 @@ Map<String, dynamic> _$SnAccountStatusInfoToJson(
|
||||
'is_disturbable': instance.isDisturbable,
|
||||
'is_online': instance.isOnline,
|
||||
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'status': instance.status?.toJson(),
|
||||
};
|
||||
|
||||
_SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountStatus(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
type: json['type'] as String,
|
||||
label: json['label'] as String,
|
||||
attitude: (json['attitude'] as num).toInt(),
|
||||
isNoDisturb: json['is_no_disturb'] as bool,
|
||||
isInvisible: json['is_invisible'] as bool,
|
||||
clearAt: json['clear_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['clear_at'] as String),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'label': instance.label,
|
||||
'attitude': instance.attitude,
|
||||
'is_no_disturb': instance.isNoDisturb,
|
||||
'is_invisible': instance.isInvisible,
|
||||
'clear_at': instance.clearAt?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
|
||||
|
@ -25,11 +25,13 @@ abstract class SnCheckInRecord with _$SnCheckInRecord {
|
||||
required int resultTier,
|
||||
required int resultExperience,
|
||||
required double resultCoin,
|
||||
@Default(0) int currentStreak,
|
||||
required List<int> resultModifiers,
|
||||
required int accountId,
|
||||
}) = _SnCheckInRecord;
|
||||
|
||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
|
||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnCheckInRecordFromJson(json);
|
||||
|
||||
String get symbol => kCheckInResultTierSymbols[resultTier];
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ mixin _$SnCheckInRecord {
|
||||
int get resultTier;
|
||||
int get resultExperience;
|
||||
double get resultCoin;
|
||||
int get currentStreak;
|
||||
List<int> get resultModifiers;
|
||||
int get accountId;
|
||||
|
||||
@ -54,6 +55,8 @@ mixin _$SnCheckInRecord {
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
(identical(other.currentStreak, currentStreak) ||
|
||||
other.currentStreak == currentStreak) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.resultModifiers, resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -71,12 +74,13 @@ mixin _$SnCheckInRecord {
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
currentStreak,
|
||||
const DeepCollectionEquality().hash(resultModifiers),
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, currentStreak: $currentStreak, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +98,7 @@ abstract mixin class $SnCheckInRecordCopyWith<$Res> {
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
int currentStreak,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -118,6 +123,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? currentStreak = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -150,6 +156,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
|
||||
? _self.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
currentStreak: null == currentStreak
|
||||
? _self.currentStreak
|
||||
: currentStreak // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _self.resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
@ -173,6 +183,7 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.resultCoin,
|
||||
this.currentStreak = 0,
|
||||
required final List<int> resultModifiers,
|
||||
required this.accountId})
|
||||
: _resultModifiers = resultModifiers,
|
||||
@ -194,6 +205,9 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
||||
final int resultExperience;
|
||||
@override
|
||||
final double resultCoin;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int currentStreak;
|
||||
final List<int> _resultModifiers;
|
||||
@override
|
||||
List<int> get resultModifiers {
|
||||
@ -238,6 +252,8 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
||||
other.resultExperience == resultExperience) &&
|
||||
(identical(other.resultCoin, resultCoin) ||
|
||||
other.resultCoin == resultCoin) &&
|
||||
(identical(other.currentStreak, currentStreak) ||
|
||||
other.currentStreak == currentStreak) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._resultModifiers, _resultModifiers) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -255,12 +271,13 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
||||
resultTier,
|
||||
resultExperience,
|
||||
resultCoin,
|
||||
currentStreak,
|
||||
const DeepCollectionEquality().hash(_resultModifiers),
|
||||
accountId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, currentStreak: $currentStreak, resultModifiers: $resultModifiers, accountId: $accountId)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,6 +297,7 @@ abstract mixin class _$SnCheckInRecordCopyWith<$Res>
|
||||
int resultTier,
|
||||
int resultExperience,
|
||||
double resultCoin,
|
||||
int currentStreak,
|
||||
List<int> resultModifiers,
|
||||
int accountId});
|
||||
}
|
||||
@ -304,6 +322,7 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
|
||||
Object? resultTier = null,
|
||||
Object? resultExperience = null,
|
||||
Object? resultCoin = null,
|
||||
Object? currentStreak = null,
|
||||
Object? resultModifiers = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -336,6 +355,10 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
|
||||
? _self.resultCoin
|
||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||
as double,
|
||||
currentStreak: null == currentStreak
|
||||
? _self.currentStreak
|
||||
: currentStreak // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
resultModifiers: null == resultModifiers
|
||||
? _self._resultModifiers
|
||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -17,6 +17,7 @@ _SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) =>
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||
currentStreak: (json['current_streak'] as num?)?.toInt() ?? 0,
|
||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
@ -32,6 +33,7 @@ Map<String, dynamic> _$SnCheckInRecordToJson(_SnCheckInRecord instance) =>
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'result_coin': instance.resultCoin,
|
||||
'current_streak': instance.currentStreak,
|
||||
'result_modifiers': instance.resultModifiers,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ class AccountImage extends StatelessWidget {
|
||||
final double? borderRadius;
|
||||
final Widget? fallbackWidget;
|
||||
final Widget? badge;
|
||||
final Offset? badgeOffset;
|
||||
|
||||
const AccountImage({
|
||||
super.key,
|
||||
@ -23,6 +24,7 @@ class AccountImage extends StatelessWidget {
|
||||
this.borderRadius,
|
||||
this.fallbackWidget,
|
||||
this.badge,
|
||||
this.badgeOffset,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -40,7 +42,8 @@ class AccountImage extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20),
|
||||
child: (content?.isEmpty ?? true)
|
||||
? Container(
|
||||
color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
|
||||
color: backgroundColor ??
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
child: (fallbackWidget ??
|
||||
Icon(
|
||||
Symbols.account_circle,
|
||||
@ -58,8 +61,8 @@ class AccountImage extends StatelessWidget {
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
right: -4,
|
||||
bottom: -2,
|
||||
right: badgeOffset?.dx ?? -4,
|
||||
bottom: badgeOffset?.dy ?? -2,
|
||||
child: badge!,
|
||||
),
|
||||
],
|
||||
|
@ -8,9 +8,9 @@ import 'package:relative_time/relative_time.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/experience.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
class AccountPopoverCard extends StatelessWidget {
|
||||
@ -72,37 +72,21 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (data.badges.isNotEmpty) const Gap(12),
|
||||
if (data.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: data.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(8),
|
||||
).padding(horizontal: 24, bottom: 12, top: 12),
|
||||
if (data.profile?.description.isNotEmpty ?? false)
|
||||
Text(
|
||||
data.profile?.description ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -110,7 +94,9 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
const Gap(8),
|
||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||
const Gap(8),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
|
||||
.fontSize(11)
|
||||
.opacity(0.5),
|
||||
const Gap(8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@ -126,25 +112,36 @@ class AccountPopoverCard extends StatelessWidget {
|
||||
FutureBuilder(
|
||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||
builder: (context, snapshot) {
|
||||
final SnAccountStatusInfo? status =
|
||||
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
|
||||
final SnAccountStatusInfo? status = snapshot.hasData
|
||||
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
||||
: null;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
(status?.isDisturbable ?? true)
|
||||
? Symbols.circle
|
||||
: Symbols.do_not_disturb_on,
|
||||
fill: (status?.isOnline ?? false) ? 1 : 0,
|
||||
size: 16,
|
||||
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
|
||||
color: (status?.isOnline ?? false)
|
||||
? (status?.isDisturbable ?? true)
|
||||
? Colors.green
|
||||
: Colors.red
|
||||
: Colors.grey,
|
||||
).padding(all: 4),
|
||||
const Gap(8),
|
||||
Text(
|
||||
status != null
|
||||
? status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
? (status.status?.label.isNotEmpty ?? false)
|
||||
? status.status!.label
|
||||
: status.isOnline
|
||||
? 'accountStatusOnline'.tr()
|
||||
: 'accountStatusOffline'.tr()
|
||||
: 'loading'.tr(),
|
||||
),
|
||||
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
||||
if (status != null &&
|
||||
!status.isOnline &&
|
||||
status.lastSeenAt != null)
|
||||
Text(
|
||||
'accountStatusLastSeen'.tr(args: [
|
||||
status.lastSeenAt != null
|
||||
|
391
lib/widgets/account/account_status.dart
Normal file
391
lib/widgets/account/account_status.dart
Normal file
@ -0,0 +1,391 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
|
||||
final Map<String, (Widget, String, String?)> kPresetStatus = {
|
||||
'online': (
|
||||
const Icon(Symbols.circle, color: Colors.green, fill: 1),
|
||||
'accountStatusOnline'.tr(),
|
||||
null,
|
||||
),
|
||||
'silent': (
|
||||
const Icon(Symbols.do_not_disturb_on, color: Colors.red),
|
||||
'accountStatusSilent'.tr(),
|
||||
'accountStatusSilentDesc'.tr(),
|
||||
),
|
||||
'invisible': (
|
||||
const Icon(Symbols.circle, color: Colors.grey),
|
||||
'accountStatusInvisible'.tr(),
|
||||
'accountStatusInvisibleDesc'.tr(),
|
||||
),
|
||||
};
|
||||
|
||||
class AccountStatusActionPopup extends StatefulWidget {
|
||||
final SnAccountStatusInfo? currentStatus;
|
||||
const AccountStatusActionPopup({super.key, this.currentStatus});
|
||||
|
||||
@override
|
||||
State<AccountStatusActionPopup> createState() =>
|
||||
_AccountStatusActionPopupState();
|
||||
}
|
||||
|
||||
class _AccountStatusActionPopupState extends State<AccountStatusActionPopup> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> setStatus(
|
||||
String type,
|
||||
String? label,
|
||||
int attitude, {
|
||||
bool isUpdate = false,
|
||||
bool isSilent = false,
|
||||
bool isInvisible = false,
|
||||
DateTime? clearAt,
|
||||
}) async {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
final payload = {
|
||||
'type': type,
|
||||
'label': label,
|
||||
'attitude': attitude,
|
||||
'is_no_disturb': isSilent,
|
||||
'is_invisible': isInvisible,
|
||||
'clear_at': clearAt?.toUtc().toIso8601String()
|
||||
};
|
||||
|
||||
try {
|
||||
await sn.client.request(
|
||||
'/cgi/id/users/me/status',
|
||||
data: payload,
|
||||
options: Options(method: isUpdate ? 'PUT' : 'POST'),
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearStatus() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/status');
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.mood, size: 24),
|
||||
const Gap(16),
|
||||
Text('accountChangeStatus',
|
||||
style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: kPresetStatus.entries
|
||||
.map(
|
||||
(x) => StyledWidget(ActionChip(
|
||||
avatar: x.value.$1,
|
||||
label: Text(x.value.$2),
|
||||
tooltip: x.value.$3,
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
setStatus(
|
||||
x.key,
|
||||
x.value.$2,
|
||||
0,
|
||||
isInvisible: x.key == 'invisible',
|
||||
isSilent: x.key == 'silent',
|
||||
);
|
||||
},
|
||||
)).padding(right: 6),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: widget.currentStatus != null
|
||||
? const Icon(Icons.edit)
|
||||
: const Icon(Icons.add),
|
||||
title: Text('accountCustomStatus').tr(),
|
||||
subtitle: Text('accountCustomStatusDescription').tr(),
|
||||
onTap: _isBusy
|
||||
? null
|
||||
: () async {
|
||||
final val = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => _AccountStatusEditorDialog(
|
||||
currentStatus: widget.currentStatus,
|
||||
),
|
||||
);
|
||||
if (val == true && context.mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.currentStatus != null)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.clear),
|
||||
title: Text('accountClearStatus').tr(),
|
||||
subtitle: Text('accountClearStatusDescription').tr(),
|
||||
onTap: _isBusy
|
||||
? null
|
||||
: () {
|
||||
_clearStatus();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountStatusEditorDialog extends StatefulWidget {
|
||||
final SnAccountStatusInfo? currentStatus;
|
||||
const _AccountStatusEditorDialog({this.currentStatus});
|
||||
|
||||
@override
|
||||
State<_AccountStatusEditorDialog> createState() =>
|
||||
_AccountStatusEditorDialogState();
|
||||
}
|
||||
|
||||
class _AccountStatusEditorDialogState
|
||||
extends State<_AccountStatusEditorDialog> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final TextEditingController _labelController = TextEditingController();
|
||||
final TextEditingController _clearAtController = TextEditingController();
|
||||
|
||||
int _attitude = 0;
|
||||
bool _isSilent = false;
|
||||
bool _isInvisible = false;
|
||||
DateTime? _clearAt;
|
||||
|
||||
Future<void> _selectClearAt() async {
|
||||
final DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _clearAt?.toLocal() ?? DateTime.now(),
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (pickedDate == null) return;
|
||||
if (!mounted) return;
|
||||
final TimeOfDay? pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
);
|
||||
if (pickedTime == null) return;
|
||||
if (!mounted) return;
|
||||
final picked = pickedDate.copyWith(
|
||||
hour: pickedTime.hour,
|
||||
minute: pickedTime.minute,
|
||||
);
|
||||
setState(() {
|
||||
_clearAt = picked;
|
||||
_clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyStatus() async {
|
||||
if (_isBusy) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.request(
|
||||
'/cgi/id/users/me/status',
|
||||
data: {
|
||||
'type': 'custom',
|
||||
'label': _labelController.text,
|
||||
'attitude': _attitude,
|
||||
'is_no_disturb': _isSilent,
|
||||
'is_invisible': _isInvisible,
|
||||
'clear_at': _clearAt?.toUtc().toIso8601String(),
|
||||
},
|
||||
options: Options(
|
||||
method: widget.currentStatus?.status != null ? 'PUT' : 'POST',
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _syncWidget() {
|
||||
if (widget.currentStatus?.status != null) {
|
||||
_clearAt = widget.currentStatus!.status!.clearAt;
|
||||
if (_clearAt != null) {
|
||||
_clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!);
|
||||
}
|
||||
|
||||
_labelController.text = widget.currentStatus!.status!.label;
|
||||
_attitude = widget.currentStatus!.status!.attitude;
|
||||
_isInvisible = widget.currentStatus!.status!.isInvisible;
|
||||
_isSilent = widget.currentStatus!.status!.isNoDisturb;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('accountCustomStatus').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
TextField(
|
||||
controller: _labelController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.label),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'fieldAccountStatusLabel'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: _clearAtController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
prefixIcon: const Icon(Icons.event_busy),
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'fieldAccountStatusClearAt'.tr(),
|
||||
),
|
||||
onTap: () => _selectClearAt(),
|
||||
),
|
||||
const Gap(8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 0,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
avatar: Icon(
|
||||
Symbols.radio_button_unchecked,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
selected: _attitude == 2,
|
||||
label: Text('accountStatusNegative'.tr()),
|
||||
onSelected: (val) {
|
||||
if (val) setState(() => _attitude = 2);
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
avatar: Icon(
|
||||
Symbols.contrast,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
selected: _attitude == 0,
|
||||
label: Text('accountStatusNeutral'.tr()),
|
||||
onSelected: (val) {
|
||||
if (val) setState(() => _attitude = 0);
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
avatar: Icon(
|
||||
Symbols.circle,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
selected: _attitude == 1,
|
||||
label: Text('accountStatusPositive'.tr()),
|
||||
onSelected: (val) {
|
||||
if (val) setState(() => _attitude = 1);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 0,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
selected: _isSilent,
|
||||
label: Text('accountStatusSilent').tr(),
|
||||
onSelected: (val) {
|
||||
setState(() => _isSilent = val);
|
||||
},
|
||||
),
|
||||
ChoiceChip(
|
||||
selected: _isInvisible,
|
||||
label: Text('accountStatusInvisible').tr(),
|
||||
onSelected: (val) {
|
||||
setState(() => _isInvisible = val);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
),
|
||||
onPressed: _isBusy ? null : () => Navigator.pop(context),
|
||||
child: Text('dialogCancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _applyStatus(),
|
||||
child: Text('dialogConfirm').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
50
lib/widgets/account/badge.dart
Normal file
50
lib/widgets/account/badge.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart' show kBadgesMeta;
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
class AccountBadge extends StatelessWidget {
|
||||
final SnAccountBadge badge;
|
||||
final double radius;
|
||||
final EdgeInsets? padding;
|
||||
const AccountBadge({
|
||||
super.key,
|
||||
required this.badge,
|
||||
this.radius = 20,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: kBadgesMeta[badge.type]?.$1.tr() ?? 'unknown'.tr()),
|
||||
if (badge.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${badge.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(badge.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: padding ?? EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
color: kBadgesMeta[badge.type]?.$3,
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
||||
color: Colors.white,
|
||||
fill: 1,
|
||||
size: radius - 4,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -22,12 +22,14 @@ class AttachmentItem extends StatelessWidget {
|
||||
final SnAttachment? data;
|
||||
final String? heroTag;
|
||||
final BoxFit fit;
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
this.fit = BoxFit.cover,
|
||||
required this.data,
|
||||
required this.heroTag,
|
||||
this.filterQuality,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
@ -47,6 +49,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
sn.getAttachmentUrl(data!.rid),
|
||||
key: Key('attachment-${data!.rid}-$tag'),
|
||||
fit: fit,
|
||||
filterQuality: filterQuality,
|
||||
),
|
||||
);
|
||||
case 'video':
|
||||
@ -83,13 +86,16 @@ class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool isCompact;
|
||||
|
||||
const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false});
|
||||
const _AttachmentItemSensitiveBlur(
|
||||
{required this.child, this.isCompact = false});
|
||||
|
||||
@override
|
||||
State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState();
|
||||
State<_AttachmentItemSensitiveBlur> createState() =>
|
||||
_AttachmentItemSensitiveBlurState();
|
||||
}
|
||||
|
||||
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> {
|
||||
class _AttachmentItemSensitiveBlurState
|
||||
extends State<_AttachmentItemSensitiveBlur> {
|
||||
bool _doesShow = false;
|
||||
|
||||
@override
|
||||
@ -124,10 +130,15 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
|
||||
Text(
|
||||
'sensitiveContentDescription',
|
||||
textAlign: TextAlign.center,
|
||||
).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)),
|
||||
)
|
||||
.tr()
|
||||
.fontSize(14)
|
||||
.textColor(Colors.white.withOpacity(0.8)),
|
||||
if (!widget.isCompact) const Gap(16),
|
||||
InkWell(
|
||||
child: Text('sensitiveContentReveal').tr().textColor(Colors.white),
|
||||
child: Text('sensitiveContentReveal')
|
||||
.tr()
|
||||
.textColor(Colors.white),
|
||||
onTap: () {
|
||||
setState(() => _doesShow = !_doesShow);
|
||||
},
|
||||
@ -137,7 +148,9 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
|
||||
).center(),
|
||||
),
|
||||
),
|
||||
).opacity(_doesShow ? 0 : 1, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||
)
|
||||
.opacity(_doesShow ? 0 : 1, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||
if (_doesShow)
|
||||
Positioned(
|
||||
top: 0,
|
||||
@ -174,10 +187,12 @@ class _AttachmentItemContentVideo extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState();
|
||||
State<_AttachmentItemContentVideo> createState() =>
|
||||
_AttachmentItemContentVideoState();
|
||||
}
|
||||
|
||||
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
|
||||
class _AttachmentItemContentVideoState
|
||||
extends State<_AttachmentItemContentVideo> {
|
||||
bool _showContent = false;
|
||||
bool _showOriginal = false;
|
||||
|
||||
@ -188,7 +203,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
setState(() => _showContent = true);
|
||||
MediaKit.ensureInitialized();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||
final url = _showOriginal
|
||||
? sn.getAttachmentUrl(widget.data.rid)
|
||||
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||
_videoPlayer = Player();
|
||||
_videoController = VideoController(_videoPlayer!);
|
||||
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
||||
@ -201,7 +218,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
_videoPlayer?.open(
|
||||
Media(
|
||||
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid),
|
||||
_showOriginal
|
||||
? sn.getAttachmentUrl(widget.data.rid)
|
||||
: sn.getAttachmentUrl(widget.data.compressed!.rid),
|
||||
),
|
||||
play: true,
|
||||
);
|
||||
@ -283,7 +302,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
),
|
||||
Text(
|
||||
Duration(
|
||||
milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000,
|
||||
milliseconds:
|
||||
(widget.data.data['duration'] ?? 0).toInt() *
|
||||
1000,
|
||||
).toString(),
|
||||
style: GoogleFonts.robotoMono(
|
||||
fontSize: 12,
|
||||
@ -346,7 +367,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
MaterialDesktopCustomButton(
|
||||
iconSize: 24,
|
||||
onPressed: _toggleOriginal,
|
||||
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
|
||||
icon: _showOriginal
|
||||
? const Icon(Symbols.high_quality, size: 24)
|
||||
: const Icon(Symbols.sd, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -354,8 +377,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
||||
child: Video(
|
||||
controller: _videoController!,
|
||||
aspectRatio: ratio,
|
||||
controls:
|
||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
|
||||
controls: !kIsWeb && (Platform.isAndroid || Platform.isIOS)
|
||||
? MaterialVideoControls
|
||||
: MaterialDesktopVideoControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -378,10 +402,12 @@ class _AttachmentItemContentAudio extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState();
|
||||
State<_AttachmentItemContentAudio> createState() =>
|
||||
_AttachmentItemContentAudioState();
|
||||
}
|
||||
|
||||
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> {
|
||||
class _AttachmentItemContentAudioState
|
||||
extends State<_AttachmentItemContentAudio> {
|
||||
bool _showContent = false;
|
||||
|
||||
double? _draggingValue;
|
||||
@ -552,8 +578,12 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
|
||||
overlayShape: SliderComponentShape.noOverlay,
|
||||
),
|
||||
child: Slider(
|
||||
secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(),
|
||||
value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(),
|
||||
secondaryTrackValue: _bufferedPosition
|
||||
.inMilliseconds
|
||||
.abs()
|
||||
.toDouble(),
|
||||
value: _draggingValue?.abs() ??
|
||||
_position.inMilliseconds.toDouble().abs(),
|
||||
min: 0,
|
||||
max: math
|
||||
.max(
|
||||
@ -593,7 +623,9 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
|
||||
),
|
||||
const Gap(16),
|
||||
IconButton.filled(
|
||||
icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow),
|
||||
icon: _isPlaying
|
||||
? const Icon(Symbols.pause)
|
||||
: const Icon(Symbols.play_arrow),
|
||||
onPressed: () {
|
||||
_audioPlayer!.playOrPause();
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ class AttachmentList extends StatefulWidget {
|
||||
final double? minWidth;
|
||||
final double? maxWidth;
|
||||
final EdgeInsets? padding;
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
const AttachmentList({
|
||||
super.key,
|
||||
@ -33,23 +34,27 @@ class AttachmentList extends StatefulWidget {
|
||||
this.minWidth,
|
||||
this.maxWidth,
|
||||
this.padding,
|
||||
this.filterQuality,
|
||||
});
|
||||
|
||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
||||
static const BorderRadius kDefaultRadius =
|
||||
BorderRadius.all(Radius.circular(8));
|
||||
|
||||
@override
|
||||
State<AttachmentList> createState() => _AttachmentListState();
|
||||
}
|
||||
|
||||
class _AttachmentListState extends State<AttachmentList> {
|
||||
late final List<String> heroTags = List.generate(widget.data.length, (_) => const Uuid().v4());
|
||||
late final List<String> heroTags =
|
||||
List.generate(widget.data.length, (_) => const Uuid().v4());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, layoutConstraints) {
|
||||
final borderSide =
|
||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
||||
final borderSide = widget.bordered
|
||||
? BorderSide(width: 1, color: Theme.of(context).dividerColor)
|
||||
: BorderSide.none;
|
||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
final constraints = BoxConstraints(
|
||||
minWidth: widget.minWidth ?? 80,
|
||||
@ -58,13 +63,13 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
|
||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||
if (widget.data.length == 1) {
|
||||
final singleAspectRatio =
|
||||
widget.data[0]?.data['ratio']?.toDouble() ??
|
||||
final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
|
||||
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
||||
'audio' => 16 / 9,
|
||||
'video' => 16 / 9,
|
||||
_ => 1,
|
||||
}.toDouble();
|
||||
}
|
||||
.toDouble();
|
||||
|
||||
return Container(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
@ -80,12 +85,18 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(data: widget.data[0], heroTag: heroTags[0], fit: widget.fit),
|
||||
child: AttachmentItem(
|
||||
data: widget.data[0],
|
||||
heroTag: heroTags[0],
|
||||
fit: widget.fit,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
|
||||
return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
@ -100,8 +111,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
final fullOfImage =
|
||||
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
||||
final fullOfImage = widget.data
|
||||
.where((ele) => ele?.mediaType == SnMediaType.image)
|
||||
.length ==
|
||||
widget.data.length;
|
||||
|
||||
if (widget.gridded && fullOfImage) {
|
||||
return Container(
|
||||
@ -117,29 +130,38 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
crossAxisCount: math.min(widget.data.length, 2),
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
children:
|
||||
widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data.where((ele) => ele != null).cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.data[idx]!.mediaType !=
|
||||
SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
.where((ele) => ele != null)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
backgroundColor: Colors.black.withOpacity(0.7),
|
||||
rootNavigator: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -156,22 +178,26 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: Column(
|
||||
children:
|
||||
widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
|
||||
),
|
||||
children: widget.data
|
||||
.mapIndexed(
|
||||
(idx, ele) => GestureDetector(
|
||||
child: AspectRatio(
|
||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
child: AttachmentItem(
|
||||
data: ele,
|
||||
heroTag: heroTags[idx],
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
)
|
||||
.expand((ele) => [ele, const Divider(height: 1)])
|
||||
.toList()
|
||||
..removeLast(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.expand((ele) => [ele, const Divider(height: 1)])
|
||||
.toList()
|
||||
..removeLast(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -179,6 +205,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||
width: double.infinity,
|
||||
child: AspectRatio(
|
||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||
child: ScrollConfiguration(
|
||||
@ -189,16 +216,22 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
itemCount: widget.data.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return Container(
|
||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
constraints:
|
||||
constraints.copyWith(maxWidth: widget.maxWidth),
|
||||
child: AspectRatio(
|
||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
aspectRatio:
|
||||
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image)
|
||||
return;
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data:
|
||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
||||
data: widget.data
|
||||
.where((ele) =>
|
||||
ele != null &&
|
||||
ele.mediaType == SnMediaType.image)
|
||||
.cast(),
|
||||
initialIndex: idx,
|
||||
heroTags: heroTags,
|
||||
),
|
||||
@ -212,18 +245,25 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border(top: borderSide, bottom: borderSide),
|
||||
border:
|
||||
Border(top: borderSide, bottom: borderSide),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
child: AttachmentItem(data: widget.data[idx], heroTag: heroTags[idx]),
|
||||
child: AttachmentItem(
|
||||
data: widget.data[idx],
|
||||
heroTag: heroTags[idx],
|
||||
filterQuality: widget.filterQuality,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(label: Text('${idx + 1}/${widget.data.length}')),
|
||||
child: Chip(
|
||||
label:
|
||||
Text('${idx + 1}/${widget.data.length}')),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -245,5 +285,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
|
||||
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};
|
||||
Set<PointerDeviceKind> get dragDevices =>
|
||||
{PointerDeviceKind.touch, PointerDeviceKind.mouse};
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' show max;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
@ -48,11 +47,14 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
bool _showOverlay = true;
|
||||
bool _dismissable = true;
|
||||
|
||||
int _page = 0;
|
||||
|
||||
void _updatePage() {
|
||||
setState(() {
|
||||
if (_isCompletedDownload) {
|
||||
setState(() => _isCompletedDownload = false);
|
||||
}
|
||||
_page = _pageController.page?.round() ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,7 +157,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
direction: _dismissable
|
||||
? DismissiblePageDismissDirection.multi
|
||||
? DismissiblePageDismissDirection.down
|
||||
: DismissiblePageDismissDirection.none,
|
||||
backgroundColor: Colors.transparent,
|
||||
isFullScreen: true,
|
||||
@ -222,31 +224,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
BoxDecoration(color: Colors.transparent),
|
||||
);
|
||||
}),
|
||||
Positioned(
|
||||
top: max(MediaQuery.of(context).padding.top, 8),
|
||||
left: 14,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_showOverlay,
|
||||
child: IconButton(
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.close),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300), Curves.easeInOut),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
height: 300,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
@ -269,153 +251,130 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Builder(builder: (context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final item = widget.data.elementAt(
|
||||
widget.data.length > 1
|
||||
? _pageController.page?.round() ?? 0
|
||||
: 0,
|
||||
);
|
||||
final account = ud.getFromCache(item.accountId);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Row(
|
||||
children: [
|
||||
if (item.accountId > 0)
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountImage(
|
||||
content: account?.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: IgnorePointer(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentUploadBy'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall,
|
||||
),
|
||||
Text(
|
||||
account?.nick ?? 'unknown'.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.data.length > 1)
|
||||
IgnorePointer(
|
||||
child: Text(
|
||||
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
|
||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||
).padding(right: 8),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
onTap: _isDownloading
|
||||
? null
|
||||
: () => _saveToAlbum(widget.data.length > 1
|
||||
? _pageController.page?.round() ?? 0
|
||||
: 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: !_isDownloading
|
||||
? !_isCompletedDownload
|
||||
? const Icon(Symbols.save_alt)
|
||||
: const Icon(Symbols.download_done)
|
||||
: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
value: _progressOfDownload,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
IgnorePointer(
|
||||
child: Text(
|
||||
item.alt,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.close),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
const Gap(2),
|
||||
IgnorePointer(
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (item.metadata['exif'] == null)
|
||||
Text(
|
||||
'#${item.rid}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (item.metadata['exif']?['Model'] != null)
|
||||
Text(
|
||||
'attachmentShotOn'.tr(args: [
|
||||
item.metadata['exif']?['Model'],
|
||||
]),
|
||||
style: metaTextStyle,
|
||||
).padding(right: 2),
|
||||
if (item.metadata['exif']?['Megapixels'] !=
|
||||
null &&
|
||||
item.metadata['exif']?['Model'] != null)
|
||||
Text(
|
||||
'${item.metadata['exif']?['Megapixels']}MP',
|
||||
style: metaTextStyle,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
item.size.formatBytes(),
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (item.metadata['width'] != null &&
|
||||
item.metadata['height'] != null)
|
||||
Text(
|
||||
'${item.metadata['width']}x${item.metadata['height']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
iconSize: 20,
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Symbols.hide).padding(all: 6),
|
||||
onPressed: () {
|
||||
setState(() => _showOverlay = false);
|
||||
}),
|
||||
Expanded(
|
||||
child: IgnorePointer(
|
||||
child: Builder(builder: (context) {
|
||||
final item = widget.data.elementAt(_page);
|
||||
final doShowCameraInfo =
|
||||
item.metadata['exif']?['Model'] != null;
|
||||
final exif = item.metadata['exif'];
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.data.length > 1)
|
||||
Text(
|
||||
'${_page + 1}/${widget.data.length}',
|
||||
style:
|
||||
GoogleFonts.robotoMono(fontSize: 13),
|
||||
).padding(right: 8),
|
||||
if (doShowCameraInfo)
|
||||
Text(
|
||||
'attachmentShotOn'
|
||||
.tr(args: [exif?['Model']]),
|
||||
style: metaTextStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (doShowCameraInfo)
|
||||
Row(
|
||||
spacing: 4,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (exif?['Megapixels'] != null)
|
||||
Text(
|
||||
'${exif?['Megapixels']}MP',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (exif?['ISO'] != null)
|
||||
Text(
|
||||
'ISO${exif['ISO']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (exif?['FNumber'] != null)
|
||||
Text(
|
||||
'f/${exif['FNumber']}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
IconButton(
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: !_isDownloading
|
||||
? !_isCompletedDownload
|
||||
? const Icon(Symbols.save_alt)
|
||||
: const Icon(Symbols.download_done)
|
||||
: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
value: _progressOfDownload,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
_isDownloading ? null : () => _saveToAlbum(_page),
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
constraints: const BoxConstraints(),
|
||||
icon: const Icon(Icons.info_outline),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surface
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _AttachmentZoomDetailPopup(
|
||||
data: widget.data.elementAt(
|
||||
widget.data.length > 1
|
||||
? _pageController.page?.round() ?? 0
|
||||
: 0),
|
||||
data: widget.data.elementAt(_page),
|
||||
),
|
||||
).then((_) {
|
||||
_showDetail = false;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'viewDetailedAttachment'.tr(),
|
||||
style: metaTextStyle.copyWith(
|
||||
decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -427,18 +386,20 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (_showOverlay) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
setState(() => _showOverlay = !_showOverlay);
|
||||
},
|
||||
onVerticalDragUpdate: (details) {
|
||||
if (_showDetail) return;
|
||||
if (_showDetail || !_dismissable) return;
|
||||
if (details.delta.dy <= -20) {
|
||||
_showDetail = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _AttachmentZoomDetailPopup(
|
||||
data: widget.data.elementAt(widget.data.length > 1
|
||||
? _pageController.page?.round() ?? 0
|
||||
: 0),
|
||||
data: widget.data.elementAt(_page),
|
||||
),
|
||||
).then((_) {
|
||||
_showDetail = false;
|
||||
|
@ -12,10 +12,10 @@ import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/keypair.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/account_popover.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/context_menu.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@ -109,18 +109,10 @@ class ChatMessage extends StatelessWidget {
|
||||
child: AccountImage(
|
||||
content: user?.avatar,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? Icon(
|
||||
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||
fill: 1,
|
||||
size: 18,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(200, 0, 0, 0)),
|
||||
],
|
||||
? AccountBadge(
|
||||
badge: user!.badges.first,
|
||||
radius: 16,
|
||||
padding: EdgeInsets.all(2),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@ -214,7 +206,8 @@ class ChatMessage extends StatelessWidget {
|
||||
data.type == 'messages.new' &&
|
||||
(data.body['text']?.isNotEmpty ?? false) &&
|
||||
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
|
||||
LinkPreviewWidget(text: data.body['text']!).padding(left: 48),
|
||||
LinkPreviewWidget(text: data.body['text']!)
|
||||
.padding(left: isCompact ? 0 : 48),
|
||||
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||
AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
|
@ -29,6 +29,7 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -43,8 +44,6 @@ import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
|
||||
|
||||
class OpenablePostItem extends StatelessWidget {
|
||||
final SnPost data;
|
||||
final bool showReactions;
|
||||
@ -149,7 +148,6 @@ class PostItem extends StatelessWidget {
|
||||
|
||||
void _doShareViaPicture(BuildContext context) async {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
context.showSnackbar('postSharingViaPicture'.tr());
|
||||
|
||||
final controller = ScreenshotController();
|
||||
final capturedImage = await controller.captureFromLongWidget(
|
||||
@ -160,9 +158,9 @@ class PostItem extends StatelessWidget {
|
||||
child: Material(
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
// Create a copy of environments
|
||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||
ChangeNotifierProvider<ConfigProvider>(
|
||||
create: (_) => context.read()),
|
||||
Provider<UserDirectoryProvider>(create: (_) => context.read()),
|
||||
],
|
||||
child: ResponsiveBreakpoints.builder(
|
||||
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
||||
@ -224,7 +222,7 @@ class PostItem extends StatelessWidget {
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
if (data.preload?.video != null)
|
||||
@ -273,7 +271,7 @@ class PostItem extends StatelessWidget {
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||
if (data.preload?.video != null)
|
||||
@ -364,7 +362,7 @@ class PostItem extends StatelessWidget {
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) onDeleted!();
|
||||
onDeleted?.call();
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
if (data.preload?.video != null)
|
||||
@ -507,6 +505,8 @@ class PostShareImageWidget extends StatelessWidget {
|
||||
StyledWidget(AttachmentList(
|
||||
data: data.preload!.attachments!,
|
||||
columned: true,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.high,
|
||||
)).padding(horizontal: 16, bottom: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -886,7 +886,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
'publisherId': data.publisherId,
|
||||
});
|
||||
if (!context.mounted) return;
|
||||
context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
|
||||
onDeleted.call();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -922,27 +922,39 @@ class _PostContentHeader extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? Icon(
|
||||
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
||||
Symbols.question_mark,
|
||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
||||
fill: 1,
|
||||
size: 18,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(200, 0, 0, 0),
|
||||
child: data.preload?.realm == null
|
||||
? AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius:
|
||||
data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||
badge: (user?.badges.isNotEmpty ?? false)
|
||||
? AccountBadge(
|
||||
badge: user!.badges.first,
|
||||
radius: 16,
|
||||
padding: EdgeInsets.all(2),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
: AccountImage(
|
||||
content: data.preload!.realm!.avatar,
|
||||
radius: isCompact ? 12 : 20,
|
||||
borderRadius: isCompact ? 4 : 8,
|
||||
badgeOffset: Offset(-6, -4),
|
||||
badge: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 2,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: AccountImage(
|
||||
content: data.publisher.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showPopover(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
@ -987,7 +999,17 @@ class _PostContentHeader extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
Row(
|
||||
children: [
|
||||
Text(data.publisher.nick).bold(),
|
||||
if (data.preload?.realm != null)
|
||||
const Icon(Symbols.arrow_right, size: 16)
|
||||
.padding(horizontal: 2)
|
||||
.opacity(0.5),
|
||||
if (data.preload?.realm != null)
|
||||
Text(data.preload!.realm!.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('@${data.publisher.name}').fontSize(13),
|
||||
@ -1037,8 +1059,10 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
queryParameters: {'editing': data.id.toString()},
|
||||
queryParameters: {
|
||||
'editing': data.id.toString(),
|
||||
'mode': data.typePlural,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -1065,8 +1089,10 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
queryParameters: {
|
||||
'replying': data.id.toString(),
|
||||
'mode': data.typePlural,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -1081,8 +1107,10 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
queryParameters: {
|
||||
'reposting': data.id.toString(),
|
||||
'mode': 'stories',
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -25,7 +25,8 @@ class PostMiniEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
|
||||
final PostWriteController _writeController =
|
||||
PostWriteController(doLoadFromTemporary: false);
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
@ -44,8 +45,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||
);
|
||||
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
||||
_writeController
|
||||
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
|
||||
_writeController.setPublisher(
|
||||
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
|
||||
_publishers?.firstOrNull);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -99,11 +101,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(item.nick).textStyle(
|
||||
Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text('@${item.name}')
|
||||
.textStyle(Theme.of(context).textTheme.bodySmall!)
|
||||
.textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!)
|
||||
.fontSize(12),
|
||||
],
|
||||
),
|
||||
@ -120,7 +128,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -129,7 +138,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text('publishersNew').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -140,7 +150,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
value: _writeController.publisher,
|
||||
onChanged: (SnPublisher? value) {
|
||||
if (value == null) {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
_publishers = null;
|
||||
_fetchPublishers();
|
||||
@ -176,7 +188,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -185,7 +198,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value, minHeight: 2),
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
@ -200,15 +214,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {
|
||||
if (widget.postReplyId != null) 'replying': widget.postReplyId.toString(),
|
||||
if (widget.postReplyId != null)
|
||||
'replying': widget.postReplyId.toString(),
|
||||
'mode': 'stories',
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: (_writeController.isBusy || _writeController.publisher == null)
|
||||
onPressed: (_writeController.isBusy ||
|
||||
_writeController.publisher == null)
|
||||
? null
|
||||
: () {
|
||||
_writeController.sendPost(context).then((_) {
|
||||
|
@ -9,10 +9,9 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/account/badge.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
|
||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
|
||||
|
||||
class PublisherPopoverCard extends StatelessWidget {
|
||||
final SnPublisher data;
|
||||
|
||||
@ -76,39 +75,22 @@ class PublisherPopoverCard extends StatelessWidget {
|
||||
const Gap(8)
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
if (user != null && user.badges.isNotEmpty) const Gap(16),
|
||||
if (user != null && user.badges.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
children: user.badges
|
||||
.map(
|
||||
(ele) => Tooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: kBadgesMeta[ele.type]?.$1.tr() ??
|
||||
'unknown'.tr()),
|
||||
if (ele.metadata['title'] != null)
|
||||
TextSpan(
|
||||
text: '\n${ele.metadata['title']}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: DateFormat.yMEd().format(ele.createdAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
|
||||
color: kBadgesMeta[ele.type]?.$3,
|
||||
fill: 1,
|
||||
),
|
||||
),
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.toList(),
|
||||
).padding(horizontal: 24),
|
||||
).padding(horizontal: 24, top: 16),
|
||||
const Gap(16),
|
||||
if (data.description.isNotEmpty)
|
||||
Text(
|
||||
data.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).padding(horizontal: 26, bottom: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
@ -34,11 +34,14 @@ class UniversalImage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||
final double? resizeHeight =
|
||||
cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||
final double? resizeWidth =
|
||||
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||
|
||||
return Image(
|
||||
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
|
||||
filterQuality:
|
||||
filterQuality ?? context.read<ConfigProvider>().imageQuality,
|
||||
image: kIsWeb
|
||||
? UniversalImage.provider(url)
|
||||
: ResizeImage(
|
||||
@ -52,7 +55,8 @@ class UniversalImage extends StatelessWidget {
|
||||
fit: fit,
|
||||
loadingBuilder: noProgressIndicator
|
||||
? null
|
||||
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxHeight: 80),
|
||||
@ -61,12 +65,15 @@ class UniversalImage extends StatelessWidget {
|
||||
tween: Tween(
|
||||
begin: 0,
|
||||
end: loadingProgress.expectedTotalBytes != null
|
||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
|
||||
value: loadingProgress.expectedTotalBytes != null
|
||||
? value.toDouble()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -114,6 +121,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
|
||||
final BoxFit? fit;
|
||||
final bool noProgressIndicator;
|
||||
final bool noErrorWidget;
|
||||
final FilterQuality? filterQuality;
|
||||
|
||||
const AutoResizeUniversalImage(
|
||||
this.url, {
|
||||
@ -123,6 +131,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
|
||||
this.fit,
|
||||
this.noProgressIndicator = false,
|
||||
this.noErrorWidget = false,
|
||||
this.filterQuality,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -137,6 +146,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
|
||||
noErrorWidget: noErrorWidget,
|
||||
cacheHeight: constraints.maxHeight,
|
||||
cacheWidth: constraints.maxWidth,
|
||||
filterQuality: filterQuality,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
56
pubspec.lock
56
pubspec.lock
@ -173,10 +173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61"
|
||||
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.4"
|
||||
version: "8.9.5"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -301,10 +301,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: croppy
|
||||
sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
|
||||
sha256: "99f4fbb4a4b44d2712e8dcd61c57c1acac151bd53cab11de3babec80407ed266"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
version: "1.3.5"
|
||||
cross_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -429,18 +429,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
|
||||
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.25.1"
|
||||
version: "2.26.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
|
||||
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.25.2"
|
||||
version: "2.26.0"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -710,6 +710,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_card_swiper:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_card_swiper
|
||||
sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -831,10 +839,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_map
|
||||
sha256: bbf145e8220531f2f727608c431871c7457f3b134e513543913afd00fdc1cd47
|
||||
sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
version: "8.1.1"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1177,10 +1185,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a"
|
||||
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+21"
|
||||
version: "0.8.12+22"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1409,10 +1417,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
|
||||
sha256: ca30ccbd97763353bde6bb1076aa4f4d17a40db0804384da77df142102aa225d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2805.1"
|
||||
version: "4.2808.0"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1601,10 +1609,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.15"
|
||||
version: "2.2.16"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1785,10 +1793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1953,10 +1961,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
|
||||
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
version: "2.4.8"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2118,10 +2126,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.4"
|
||||
version: "2.7.5"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2326,10 +2334,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.14"
|
||||
version: "6.3.15"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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.4.2+77
|
||||
version: 2.4.2+78
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -138,6 +138,7 @@ dependencies:
|
||||
flutter_map: ^8.1.0
|
||||
geolocator: ^13.0.2
|
||||
fast_rsa: ^3.8.0
|
||||
flutter_card_swiper: ^7.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user