Compare commits
31 Commits
ce6e9c185a
...
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 | |||
54c098c274 | |||
29731728cd | |||
9e8882c580 | |||
6042e57e7a | |||
6235e736b9 | |||
e075804782 | |||
d40a6ca1c4 | |||
5ac657e526 | |||
97ddc18b8e | |||
b835c8edea | |||
288c0399f9 | |||
1478933cf1 | |||
93c6fa6e53 |
BIN
assets/fonts/Nunito-Bold.ttf
Executable file
BIN
assets/fonts/Nunito-Bold.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Nunito-Italic.ttf
Executable file
BIN
assets/fonts/Nunito-Italic.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Nunito-Regular.ttf
Executable file
BIN
assets/fonts/Nunito-Regular.ttf
Executable file
Binary file not shown.
@ -153,6 +153,11 @@
|
|||||||
"publisherRunBy": "Run by {}",
|
"publisherRunBy": "Run by {}",
|
||||||
"fieldPublisherBelongToRealm": "Belongs to",
|
"fieldPublisherBelongToRealm": "Belongs to",
|
||||||
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
|
||||||
|
"writePost": "Compose",
|
||||||
|
"postTypeStory": "Story",
|
||||||
|
"postTypeArticle": "Article",
|
||||||
|
"postTypeQuestion": "Question",
|
||||||
|
"postTypeVideo": "Video",
|
||||||
"writePostTypeStory": "Post a story",
|
"writePostTypeStory": "Post a story",
|
||||||
"writePostTypeArticle": "Write an article",
|
"writePostTypeArticle": "Write an article",
|
||||||
"writePostTypeQuestion": "Ask a question",
|
"writePostTypeQuestion": "Ask a question",
|
||||||
@ -763,5 +768,28 @@
|
|||||||
"decrypting": "Decrypting……",
|
"decrypting": "Decrypting……",
|
||||||
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online",
|
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online",
|
||||||
"messageUnablePreview": "Unable preview",
|
"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": "由 {} 管理",
|
"publisherRunBy": "由 {} 管理",
|
||||||
"fieldPublisherBelongToRealm": "所属领域",
|
"fieldPublisherBelongToRealm": "所属领域",
|
||||||
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
|
||||||
|
"writePost": "撰写",
|
||||||
|
"postTypeStory": "动态",
|
||||||
|
"postTypeArticle": "文章",
|
||||||
|
"postTypeQuestion": "问题",
|
||||||
|
"postTypeVideo": "视频",
|
||||||
"writePostTypeStory": "发动态",
|
"writePostTypeStory": "发动态",
|
||||||
"writePostTypeArticle": "写文章",
|
"writePostTypeArticle": "写文章",
|
||||||
"writePostTypeQuestion": "提问题",
|
"writePostTypeQuestion": "提问题",
|
||||||
@ -761,5 +766,28 @@
|
|||||||
"decrypting": "解密中……",
|
"decrypting": "解密中……",
|
||||||
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线",
|
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线",
|
||||||
"messageUnablePreview": "无法预览消息",
|
"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": "由 {} 管理",
|
"publisherRunBy": "由 {} 管理",
|
||||||
"fieldPublisherBelongToRealm": "所屬領域",
|
"fieldPublisherBelongToRealm": "所屬領域",
|
||||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||||
|
"writePost": "撰寫",
|
||||||
|
"postTypeStory": "動態",
|
||||||
|
"postTypeArticle": "文章",
|
||||||
|
"postTypeQuestion": "問題",
|
||||||
|
"postTypeVideo": "視頻",
|
||||||
"writePostTypeStory": "發動態",
|
"writePostTypeStory": "發動態",
|
||||||
"writePostTypeArticle": "寫文章",
|
"writePostTypeArticle": "寫文章",
|
||||||
"writePostTypeQuestion": "提問題",
|
"writePostTypeQuestion": "提問題",
|
||||||
@ -761,5 +766,28 @@
|
|||||||
"decrypting": "解密中……",
|
"decrypting": "解密中……",
|
||||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
||||||
"messageUnablePreview": "無法預覽消息",
|
"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": "由 {} 管理",
|
"publisherRunBy": "由 {} 管理",
|
||||||
"fieldPublisherBelongToRealm": "所屬領域",
|
"fieldPublisherBelongToRealm": "所屬領域",
|
||||||
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||||
|
"writePost": "撰寫",
|
||||||
|
"postTypeStory": "動態",
|
||||||
|
"postTypeArticle": "文章",
|
||||||
|
"postTypeQuestion": "問題",
|
||||||
|
"postTypeVideo": "視頻",
|
||||||
"writePostTypeStory": "發動態",
|
"writePostTypeStory": "發動態",
|
||||||
"writePostTypeArticle": "寫文章",
|
"writePostTypeArticle": "寫文章",
|
||||||
"writePostTypeQuestion": "提問題",
|
"writePostTypeQuestion": "提問題",
|
||||||
@ -761,5 +766,28 @@
|
|||||||
"decrypting": "解密中……",
|
"decrypting": "解密中……",
|
||||||
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
|
||||||
"messageUnablePreview": "無法預覽消息",
|
"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": "正面"
|
||||||
}
|
}
|
||||||
|
1
drift_schemas/my_database/drift_schema_v3.json
Normal file
1
drift_schemas/my_database/drift_schema_v3.json
Normal file
File diff suppressed because one or more lines are too long
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/database/database.dart';
|
import 'package:surface/database/database.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/database.dart';
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/keypair.dart';
|
import 'package:surface/providers/keypair.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
@ -26,6 +27,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
late final WebSocketProvider _ws;
|
late final WebSocketProvider _ws;
|
||||||
late final SnAttachmentProvider _attach;
|
late final SnAttachmentProvider _attach;
|
||||||
late final DatabaseProvider _dt;
|
late final DatabaseProvider _dt;
|
||||||
|
late final ChatChannelProvider _ct;
|
||||||
late final KeyPairProvider _kp;
|
late final KeyPairProvider _kp;
|
||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
StreamSubscription? _wsSubscription;
|
||||||
@ -35,6 +37,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
_ws = context.read<WebSocketProvider>();
|
_ws = context.read<WebSocketProvider>();
|
||||||
_attach = context.read<SnAttachmentProvider>();
|
_attach = context.read<SnAttachmentProvider>();
|
||||||
|
_ct = context.read<ChatChannelProvider>();
|
||||||
_dt = context.read<DatabaseProvider>();
|
_dt = context.read<DatabaseProvider>();
|
||||||
_kp = context.read<KeyPairProvider>();
|
_kp = context.read<KeyPairProvider>();
|
||||||
}
|
}
|
||||||
@ -65,10 +68,7 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
channel = chan;
|
channel = chan;
|
||||||
|
|
||||||
// Fetch channel profile
|
// Fetch channel profile
|
||||||
final resp = await _sn.client.get(
|
profile = await _ct.getChannelProfile(channel!);
|
||||||
'/cgi/im/channels/${chan.keyPath}/me',
|
|
||||||
);
|
|
||||||
profile = SnChannelMember.fromJson(resp.data);
|
|
||||||
|
|
||||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
@ -287,23 +287,26 @@ class ChatMessageController extends ChangeNotifier {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock the message locally
|
// Mock the message locally
|
||||||
final createdAt = DateTime.now();
|
// Do not mock the editing message
|
||||||
final message = SnChatMessage(
|
if (editingMessage == null) {
|
||||||
id: 0,
|
final createdAt = DateTime.now();
|
||||||
createdAt: createdAt,
|
final message = SnChatMessage(
|
||||||
updatedAt: createdAt,
|
id: 0,
|
||||||
deletedAt: null,
|
createdAt: createdAt,
|
||||||
uuid: nonce,
|
updatedAt: createdAt,
|
||||||
body: body,
|
deletedAt: null,
|
||||||
type: type,
|
uuid: nonce,
|
||||||
channel: channel!,
|
body: body,
|
||||||
channelId: channel!.id,
|
type: type,
|
||||||
sender: profile!,
|
channel: channel!,
|
||||||
senderId: profile!.id,
|
channelId: channel!.id,
|
||||||
quoteEventId: quoteId,
|
sender: profile!,
|
||||||
relatedEventId: relatedId,
|
senderId: profile!.id,
|
||||||
);
|
quoteEventId: quoteId,
|
||||||
_addUnconfirmedMessage(message);
|
relatedEventId: relatedId,
|
||||||
|
);
|
||||||
|
_addUnconfirmedMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
// Send to server
|
// Send to server
|
||||||
try {
|
try {
|
||||||
|
@ -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;
|
bool get isEmpty => attachment == null && file == null && raw == null;
|
||||||
|
|
||||||
@ -105,7 +106,8 @@ class PostWriteMedia {
|
|||||||
}) {
|
}) {
|
||||||
if (attachment != null) {
|
if (attachment != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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) {
|
if (width != null && height != null && !kIsWeb) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
@ -116,7 +118,8 @@ class PostWriteMedia {
|
|||||||
}
|
}
|
||||||
return provider;
|
return provider;
|
||||||
} else if (file != null) {
|
} 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) {
|
if (width != null && height != null) {
|
||||||
return ResizeImage(
|
return ResizeImage(
|
||||||
provider,
|
provider,
|
||||||
@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
final TextEditingController aliasController = TextEditingController();
|
final TextEditingController aliasController = TextEditingController();
|
||||||
final TextEditingController rewardController = TextEditingController();
|
final TextEditingController rewardController = TextEditingController();
|
||||||
|
|
||||||
ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
|
ContentInsertionConfiguration get contentInsertionConfiguration =>
|
||||||
|
ContentInsertionConfiguration(
|
||||||
onContentInserted: (KeyboardInsertedContent content) {
|
onContentInserted: (KeyboardInsertedContent content) {
|
||||||
if (content.hasData) {
|
if (content.hasData) {
|
||||||
addAttachments(
|
addAttachments([
|
||||||
[PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
|
PostWriteMedia.fromBytes(content.data!,
|
||||||
|
'attachmentInsertedImage'.tr(), SnMediaType.image)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
|
|
||||||
String get description => descriptionController.text;
|
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;
|
bool isLoading = false, isBusy = false;
|
||||||
double? progress;
|
double? progress;
|
||||||
@ -201,6 +208,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
SnRealm? realm;
|
SnRealm? realm;
|
||||||
SnPublisher? publisher;
|
SnPublisher? publisher;
|
||||||
SnPost? editingPost, repostingPost, replyingPost;
|
SnPost? editingPost, repostingPost, replyingPost;
|
||||||
|
bool editingDraft = false;
|
||||||
|
|
||||||
int visibility = 0;
|
int visibility = 0;
|
||||||
List<int> visibleUsers = List.empty();
|
List<int> visibleUsers = List.empty();
|
||||||
@ -237,14 +245,20 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||||
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
|
invisibleUsers =
|
||||||
|
List.from(post.invisibleUsersList ?? [], growable: true);
|
||||||
visibility = post.visibility;
|
visibility = post.visibility;
|
||||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
categories =
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||||
|
attachments.addAll(
|
||||||
|
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
poll = post.preload?.poll;
|
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);
|
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||||
}
|
}
|
||||||
if (post.preload?.realm != null) {
|
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 {
|
{bool isCompressed = false}) async {
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
@ -281,7 +296,9 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
media.name,
|
media.name,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
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(
|
var item = await attach.chunkedUploadParts(
|
||||||
@ -297,9 +314,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||||
try {
|
try {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
final compressedAttachment =
|
||||||
|
await _tryCompressVideoCopy(context, media);
|
||||||
if (compressedAttachment != null) {
|
if (compressedAttachment != null) {
|
||||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
item = await attach.updateOne(item,
|
||||||
|
compressedId: compressedAttachment.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
@ -309,8 +328,10 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
|
Future<SnAttachment?> _tryCompressVideoCopy(
|
||||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
|
BuildContext context, PostWriteMedia media) async {
|
||||||
|
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
|
||||||
|
return null;
|
||||||
if (media.type != SnMediaType.video) return null;
|
if (media.type != SnMediaType.video) return null;
|
||||||
if (media.file == null) return null;
|
if (media.file == null) return null;
|
||||||
if (VideoCompress.isCompressing) return null;
|
if (VideoCompress.isCompressing) return null;
|
||||||
@ -334,7 +355,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (!context.mounted) return null;
|
if (!context.mounted) return null;
|
||||||
|
|
||||||
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
|
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;
|
return compressedAttachment;
|
||||||
}
|
}
|
||||||
@ -370,18 +392,25 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
'content': contentController.text,
|
'content': contentController.text,
|
||||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||||
if (titleController.text.isNotEmpty) 'title': titleController.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 (rewardController.text.isNotEmpty) 'reward': rewardController.text,
|
||||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
|
if (thumbnail != null && thumbnail!.attachment != null)
|
||||||
'attachments':
|
'thumbnail': thumbnail!.attachment!.toJson(),
|
||||||
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
|
'attachments': attachments
|
||||||
|
.where((e) => e.attachment != null)
|
||||||
|
.map((e) => e.attachment!.toJson())
|
||||||
|
.toList(growable: true),
|
||||||
'tags': tags.map((ele) => {'alias': ele}).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,
|
'visibility': visibility,
|
||||||
'visible_users_list': visibleUsers,
|
'visible_users_list': visibleUsers,
|
||||||
'invisible_users_list': invisibleUsers,
|
'invisible_users_list': invisibleUsers,
|
||||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
if (publishedAt != null)
|
||||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (publishedUntil != null)
|
||||||
|
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||||
if (poll != null) 'poll': poll!.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;
|
bool temporaryRestored = false;
|
||||||
|
|
||||||
void _temporaryLoad() {
|
void _temporaryLoad() {
|
||||||
@ -403,18 +438,24 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
titleController.text = data['title'] ?? '';
|
titleController.text = data['title'] ?? '';
|
||||||
descriptionController.text = data['description'] ?? '';
|
descriptionController.text = data['description'] ?? '';
|
||||||
rewardController.text = data['reward']?.toString() ?? '';
|
rewardController.text = data['reward']?.toString() ?? '';
|
||||||
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
if (data['thumbnail'] != null)
|
||||||
attachments
|
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
|
||||||
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
|
attachments.addAll(data['attachments']
|
||||||
|
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
|
||||||
|
.cast<PostWriteMedia>());
|
||||||
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
tags = List.from(data['tags'].map((ele) => ele['alias']));
|
||||||
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
categories = List.from(data['categories'].map((ele) => ele['alias']));
|
||||||
visibility = data['visibility'];
|
visibility = data['visibility'];
|
||||||
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
visibleUsers = List.from(data['visible_users_list'] ?? []);
|
||||||
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
|
||||||
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
if (data['published_at'] != null)
|
||||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
|
||||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
if (data['published_until'] != null)
|
||||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : 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;
|
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||||
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
|
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
|
||||||
temporaryRestored = true;
|
temporaryRestored = true;
|
||||||
@ -436,7 +477,10 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> sendPost(BuildContext context) async {
|
Future<void> sendPost(
|
||||||
|
BuildContext context, {
|
||||||
|
bool saveAsDraft = false,
|
||||||
|
}) async {
|
||||||
if (isBusy || publisher == null) return;
|
if (isBusy || publisher == null) return;
|
||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
@ -463,7 +507,9 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
media.name,
|
media.name,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
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(
|
var item = await attach.chunkedUploadParts(
|
||||||
@ -472,16 +518,20 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
place.$2,
|
place.$2,
|
||||||
onProgress: (value) {
|
onProgress: (value) {
|
||||||
// Calculate overall progress for attachments
|
// Calculate overall progress for attachments
|
||||||
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
progress = math.max(
|
||||||
|
((i + value) / attachments.length) * kAttachmentProgressWeight,
|
||||||
|
value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
final compressedAttachment =
|
||||||
|
await _tryCompressVideoCopy(context, media);
|
||||||
if (compressedAttachment != null) {
|
if (compressedAttachment != null) {
|
||||||
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
item = await attach.updateOne(item,
|
||||||
|
compressedId: compressedAttachment.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -508,7 +558,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
// Posting the content
|
// Posting the content
|
||||||
try {
|
try {
|
||||||
final baseProgressVal = progress!;
|
final baseProgressVal = progress!;
|
||||||
await sn.client.request(
|
final resp = await sn.client.request(
|
||||||
[
|
[
|
||||||
'/cgi/co/$mode',
|
'/cgi/co/$mode',
|
||||||
if (editingPost != null) '${editingPost!.id}',
|
if (editingPost != null) '${editingPost!.id}',
|
||||||
@ -518,36 +568,56 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
'content': contentController.text,
|
'content': contentController.text,
|
||||||
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
|
||||||
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
if (titleController.text.isNotEmpty) 'title': titleController.text,
|
||||||
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
|
if (descriptionController.text.isNotEmpty)
|
||||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
'description': descriptionController.text,
|
||||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
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(),
|
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||||
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||||
'visibility': visibility,
|
'visibility': visibility,
|
||||||
'visible_users_list': visibleUsers,
|
'visible_users_list': visibleUsers,
|
||||||
'invisible_users_list': invisibleUsers,
|
'invisible_users_list': invisibleUsers,
|
||||||
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
|
if (publishedAt != null)
|
||||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
|
if (publishedUntil != null)
|
||||||
|
'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||||
if (reward != null) 'reward': reward,
|
if (reward != null) 'reward': reward,
|
||||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||||
if (poll != null) 'poll': poll!.id,
|
if (poll != null) 'poll': poll!.id,
|
||||||
if (realm != null) 'realm': realm!.id,
|
if (realm != null) 'realm': realm!.id,
|
||||||
|
'is_draft': saveAsDraft,
|
||||||
},
|
},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
progress =
|
||||||
|
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onReceiveProgress: (count, total) {
|
onReceiveProgress: (count, total) {
|
||||||
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
|
progress = baseProgressVal +
|
||||||
|
(kPostingProgressWeight / 2) +
|
||||||
|
(count / total) * (kPostingProgressWeight / 2);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
method: editingPost != null ? 'PUT' : 'POST',
|
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) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -683,7 +753,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
repostingPost = null;
|
repostingPost = null;
|
||||||
mode = kTitleMap.keys.first;
|
mode = kTitleMap.keys.first;
|
||||||
temporaryRestored = false;
|
temporaryRestored = false;
|
||||||
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
|
SharedPreferences.getInstance()
|
||||||
|
.then((prefs) => prefs.remove(kTemporaryStorageKey));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
42
lib/database/account.dart
Normal file
42
lib/database/account.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
|
class SnAccountConverter extends TypeConverter<SnAccount, String>
|
||||||
|
with JsonTypeConverter2<SnAccount, String, Map<String, Object?>> {
|
||||||
|
const SnAccountConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnAccount fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnAccount value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnAccount fromJson(Map<String, Object?> json) {
|
||||||
|
return SnAccount.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnAccount value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TableIndex(name: 'idx_account_name', columns: {#name})
|
||||||
|
class SnLocalAccount extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
TextColumn get name => text()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(const SnAccountConverter())();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||||
|
}
|
47
lib/database/attachment.dart
Normal file
47
lib/database/attachment.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
|
class SnAttachmentConverter extends TypeConverter<SnAttachment, String>
|
||||||
|
with JsonTypeConverter2<SnAttachment, String, Map<String, Object?>> {
|
||||||
|
const SnAttachmentConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnAttachment fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnAttachment value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnAttachment fromJson(Map<String, Object?> json) {
|
||||||
|
return SnAttachment.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnAttachment value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TableIndex(name: 'idx_attachment_rid', columns: {#rid})
|
||||||
|
@TableIndex(name: 'idx_attachment_account', columns: {#accountId})
|
||||||
|
class SnLocalAttachment extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
TextColumn get rid => text().unique()();
|
||||||
|
|
||||||
|
TextColumn get uuid => text().unique()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(const SnAttachmentConverter())();
|
||||||
|
|
||||||
|
IntColumn get accountId => integer()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||||
|
}
|
@ -28,6 +28,7 @@ class SnChannelConverter extends TypeConverter<SnChannel, String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TableIndex(name: 'idx_channel_alias', columns: {#alias})
|
||||||
class SnLocalChatChannel extends Table {
|
class SnLocalChatChannel extends Table {
|
||||||
IntColumn get id => integer().autoIncrement()();
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
@ -63,12 +64,54 @@ class SnMessageConverter extends TypeConverter<SnChatMessage, String>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TableIndex(name: 'idx_chat_channel', columns: {#channelId})
|
||||||
class SnLocalChatMessage extends Table {
|
class SnLocalChatMessage extends Table {
|
||||||
IntColumn get id => integer().autoIncrement()();
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
IntColumn get channelId => integer()();
|
IntColumn get channelId => integer()();
|
||||||
|
|
||||||
|
IntColumn get senderId => integer().nullable()();
|
||||||
|
|
||||||
TextColumn get content => text().map(const SnMessageConverter())();
|
TextColumn get content => text().map(const SnMessageConverter())();
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SnChannelMemberConverter extends TypeConverter<SnChannelMember, String>
|
||||||
|
with JsonTypeConverter2<SnChannelMember, String, Map<String, Object?>> {
|
||||||
|
const SnChannelMemberConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnChannelMember fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnChannelMember value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnChannelMember fromJson(Map<String, Object?> json) {
|
||||||
|
return SnChannelMember.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnChannelMember value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnLocalChannelMember extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
IntColumn get channelId => integer()();
|
||||||
|
|
||||||
|
IntColumn get accountId => integer()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(SnChannelMemberConverter())();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||||
|
}
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_flutter/drift_flutter.dart';
|
import 'package:drift_flutter/drift_flutter.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:surface/database/account.dart';
|
||||||
|
import 'package:surface/database/attachment.dart';
|
||||||
import 'package:surface/database/chat.dart';
|
import 'package:surface/database/chat.dart';
|
||||||
import 'package:surface/database/database.steps.dart';
|
import 'package:surface/database/database.steps.dart';
|
||||||
import 'package:surface/database/keypair.dart';
|
import 'package:surface/database/keypair.dart';
|
||||||
|
import 'package:surface/database/sticker.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@DriftDatabase(tables: [SnLocalChatChannel, SnLocalChatMessage, SnLocalKeyPair])
|
@DriftDatabase(tables: [
|
||||||
|
SnLocalChatChannel,
|
||||||
|
SnLocalChatMessage,
|
||||||
|
SnLocalChannelMember,
|
||||||
|
SnLocalKeyPair,
|
||||||
|
SnLocalAccount,
|
||||||
|
SnLocalAttachment,
|
||||||
|
SnLocalSticker,
|
||||||
|
SnLocalStickerPack,
|
||||||
|
])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
@ -33,6 +47,8 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
return MigrationStrategy(
|
return MigrationStrategy(
|
||||||
onUpgrade: stepByStep(from1To2: (m, schema) async {
|
onUpgrade: stepByStep(from1To2: (m, schema) async {
|
||||||
// Nothing else to do here
|
// Nothing else to do here
|
||||||
|
}, from2To3: (m, schema) async {
|
||||||
|
// Nothing else to do here, too
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -140,8 +140,281 @@ i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
|
|||||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
'CHECK ("is_active" IN (0, 1))'),
|
'CHECK ("is_active" IN (0, 1))'),
|
||||||
defaultValue: const CustomExpression('0'));
|
defaultValue: const CustomExpression('0'));
|
||||||
|
|
||||||
|
final class Schema3 extends i0.VersionedSchema {
|
||||||
|
Schema3({required super.database}) : super(version: 3);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
snLocalChatChannel,
|
||||||
|
snLocalChatMessage,
|
||||||
|
snLocalChannelMember,
|
||||||
|
snLocalKeyPair,
|
||||||
|
snLocalAccount,
|
||||||
|
snLocalAttachment,
|
||||||
|
snLocalSticker,
|
||||||
|
snLocalStickerPack,
|
||||||
|
idxChannelAlias,
|
||||||
|
idxChatChannel,
|
||||||
|
idxAccountName,
|
||||||
|
idxAttachmentRid,
|
||||||
|
idxAttachmentAccount,
|
||||||
|
];
|
||||||
|
late final Shape0 snLocalChatChannel = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_chat_channel',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape3 snLocalChatMessage = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_chat_message',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_4,
|
||||||
|
_column_10,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape4 snLocalChannelMember = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_channel_member',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_4,
|
||||||
|
_column_6,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape2 snLocalKeyPair = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_key_pair',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [
|
||||||
|
'PRIMARY KEY(id)',
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 snLocalAccount = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_account',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_12,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 snLocalAttachment = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_attachment',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_2,
|
||||||
|
_column_6,
|
||||||
|
_column_3,
|
||||||
|
_column_11,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 snLocalSticker = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_sticker',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_15,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 snLocalStickerPack = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'sn_local_sticker_pack',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
|
||||||
|
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||||
|
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
|
||||||
|
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
|
||||||
|
final i1.Index idxAccountName = i1.Index('idx_account_name',
|
||||||
|
'CREATE INDEX idx_account_name ON sn_local_account (name)');
|
||||||
|
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
|
||||||
|
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||||
|
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
|
||||||
|
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape3 extends i0.VersionedTable {
|
||||||
|
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get channelId =>
|
||||||
|
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get senderId =>
|
||||||
|
columnsByName['sender_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<int>('sender_id', aliasedName, true,
|
||||||
|
type: i1.DriftSqlType.int);
|
||||||
|
|
||||||
|
class Shape4 extends i0.VersionedTable {
|
||||||
|
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get channelId =>
|
||||||
|
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||||
|
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.dateTime);
|
||||||
|
|
||||||
|
class Shape5 extends i0.VersionedTable {
|
||||||
|
Shape5({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||||
|
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
|
||||||
|
class Shape6 extends i0.VersionedTable {
|
||||||
|
Shape6({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get rid =>
|
||||||
|
columnsByName['rid']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get uuid =>
|
||||||
|
columnsByName['uuid']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get accountId =>
|
||||||
|
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||||
|
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('rid', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||||
|
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('uuid', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||||
|
|
||||||
|
class Shape7 extends i0.VersionedTable {
|
||||||
|
Shape7({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get alias =>
|
||||||
|
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get fullAlias =>
|
||||||
|
columnsByName['full_alias']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('full_alias', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
|
||||||
|
class Shape8 extends i0.VersionedTable {
|
||||||
|
Shape8({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get content =>
|
||||||
|
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
}
|
||||||
|
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@ -150,6 +423,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from1To2(migrator, schema);
|
await from1To2(migrator, schema);
|
||||||
return 2;
|
return 2;
|
||||||
|
case 2:
|
||||||
|
final schema = Schema3(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from2To3(migrator, schema);
|
||||||
|
return 3;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@ -158,8 +436,10 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
|
|
||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
|
from2To3: from2To3,
|
||||||
));
|
));
|
||||||
|
74
lib/database/sticker.dart
Normal file
74
lib/database/sticker.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
|
class SnStickerConverter extends TypeConverter<SnSticker, String>
|
||||||
|
with JsonTypeConverter2<SnSticker, String, Map<String, Object?>> {
|
||||||
|
const SnStickerConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnSticker fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnSticker value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnSticker fromJson(Map<String, Object?> json) {
|
||||||
|
return SnSticker.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnSticker value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnLocalSticker extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
TextColumn get alias => text()();
|
||||||
|
|
||||||
|
TextColumn get fullAlias => text()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(const SnStickerConverter())();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnStickerPackConverter extends TypeConverter<SnStickerPack, String>
|
||||||
|
with JsonTypeConverter2<SnStickerPack, String, Map<String, Object?>> {
|
||||||
|
const SnStickerPackConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnStickerPack fromSql(String fromDb) {
|
||||||
|
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toSql(SnStickerPack value) {
|
||||||
|
return jsonEncode(toJson(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
SnStickerPack fromJson(Map<String, Object?> json) {
|
||||||
|
return SnStickerPack.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Object?> toJson(SnStickerPack value) {
|
||||||
|
return value.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnLocalStickerPack extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
|
||||||
|
TextColumn get content => text().map(const SnStickerPackConverter())();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
@ -314,6 +314,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listSticker();
|
await sticker.listSticker();
|
||||||
|
if (!mounted) return;
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
final userCacheSize = await ud.loadAccountCache();
|
||||||
|
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
||||||
logging.info('[Bootstrap] Everything initialized!');
|
logging.info('[Bootstrap] Everything initialized!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -491,6 +495,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
cfg.calcDrawerSize(context);
|
cfg.calcDrawerSize(context);
|
||||||
});
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (context.mounted) {
|
||||||
|
cfg.calcDrawerSize(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
return SizeChangedLayoutNotifier(
|
return SizeChangedLayoutNotifier(
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@ import 'package:surface/providers/database.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/sn_realm.dart';
|
import 'package:surface/providers/sn_realm.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
|
|
||||||
class ChatChannelProvider extends ChangeNotifier {
|
class ChatChannelProvider extends ChangeNotifier {
|
||||||
@ -15,12 +16,14 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final UserDirectoryProvider _ud;
|
late final UserDirectoryProvider _ud;
|
||||||
|
late final UserProvider _ua;
|
||||||
late final DatabaseProvider _dt;
|
late final DatabaseProvider _dt;
|
||||||
late final SnRealmProvider _rels;
|
late final SnRealmProvider _rels;
|
||||||
|
|
||||||
ChatChannelProvider(BuildContext context) {
|
ChatChannelProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_ud = context.read<UserDirectoryProvider>();
|
_ud = context.read<UserDirectoryProvider>();
|
||||||
|
_ua = context.read<UserProvider>();
|
||||||
_dt = context.read<DatabaseProvider>();
|
_dt = context.read<DatabaseProvider>();
|
||||||
_rels = context.read<SnRealmProvider>();
|
_rels = context.read<SnRealmProvider>();
|
||||||
}
|
}
|
||||||
@ -149,4 +152,60 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
|
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveMemberToLocal(Iterable<SnChannelMember> members) async {
|
||||||
|
final queries = members.map((ele) {
|
||||||
|
return _dt.db.snLocalChannelMember.insertOne(
|
||||||
|
SnLocalChannelMemberCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
channelId: ele.channelId,
|
||||||
|
accountId: ele.accountId,
|
||||||
|
content: ele,
|
||||||
|
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalChannelMemberCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
cacheExpiredAt:
|
||||||
|
Constant(DateTime.now().add(const Duration(days: 7))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Future.wait(queries);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeLocalChannel(SnChannel channel) async {
|
||||||
|
await _dt.db.transaction(() async {
|
||||||
|
await (_dt.db.snLocalChannelMember.delete()
|
||||||
|
..where((e) => e.channelId.equals(channel.id)))
|
||||||
|
.go();
|
||||||
|
await (_dt.db.snLocalChatChannel.delete()
|
||||||
|
..where((e) => e.id.equals(channel.id)))
|
||||||
|
.go();
|
||||||
|
await (_dt.db.snLocalChatMessage.delete()
|
||||||
|
..where((e) => e.channelId.equals(channel.id)))
|
||||||
|
.go();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateChannelProfile(SnChannelMember member) {
|
||||||
|
return _saveMemberToLocal([member]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnChannelMember> getChannelProfile(SnChannel channel) async {
|
||||||
|
if (_ua.user == null) throw Exception('User not logged in');
|
||||||
|
final local = await (_dt.db.snLocalChannelMember.select()
|
||||||
|
..where((e) => e.channelId.equals(channel.id))
|
||||||
|
..where((e) => e.accountId.equals(_ua.user!.id)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (local != null) {
|
||||||
|
return local.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resp = await _sn.client.get('/cgi/im/channels/${channel.keyPath}/me');
|
||||||
|
final out = SnChannelMember.fromJson(resp.data);
|
||||||
|
_saveMemberToLocal([out]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,16 +60,24 @@ class SnPostContentProvider {
|
|||||||
|
|
||||||
out[i] = out[i].copyWith(
|
out[i] = out[i].copyWith(
|
||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
thumbnail: attachments
|
||||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
.where((ele) => ele?.rid == out[i].body['thumbnail'])
|
||||||
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
.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,
|
poll: poll,
|
||||||
realm: realm,
|
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);
|
await _ud.listAccount(uids);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@ -107,15 +115,23 @@ class SnPostContentProvider {
|
|||||||
|
|
||||||
out = out.copyWith(
|
out = out.copyWith(
|
||||||
preload: SnPostPreload(
|
preload: SnPostPreload(
|
||||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
thumbnail: attachments
|
||||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
.where((ele) => ele?.rid == out.body['thumbnail'])
|
||||||
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
.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,
|
poll: poll,
|
||||||
realm: realm,
|
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);
|
await _ud.listAccount(uids);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@ -138,17 +154,25 @@ class SnPostContentProvider {
|
|||||||
Iterable<String>? tags,
|
Iterable<String>? tags,
|
||||||
String? realm,
|
String? realm,
|
||||||
String? channel,
|
String? channel,
|
||||||
|
bool isDraft = false,
|
||||||
|
bool isShuffle = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
final resp = await _sn.client.get(
|
||||||
'take': take,
|
isShuffle
|
||||||
'offset': offset,
|
? '/cgi/co/recommendations/shuffle'
|
||||||
if (type != null) 'type': type,
|
: '/cgi/co/posts${isDraft ? '/drafts' : ''}',
|
||||||
if (author != null) 'author': author,
|
queryParameters: {
|
||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
'take': take,
|
||||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
'offset': offset,
|
||||||
if (realm != null) 'realm': realm,
|
if (type != null) 'type': type,
|
||||||
if (channel != null) 'channel': channel,
|
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(
|
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||||
);
|
);
|
||||||
@ -161,7 +185,8 @@ class SnPostContentProvider {
|
|||||||
int take = 10,
|
int take = 10,
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
}) async {
|
}) 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,
|
'take': take,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
});
|
});
|
||||||
@ -200,4 +225,9 @@ class SnPostContentProvider {
|
|||||||
);
|
);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SnPost> completePostData(SnPost post) async {
|
||||||
|
final out = await _preloadRelatedDataSingle(post);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
@ -13,10 +16,12 @@ const kConcurrentUploadChunks = 5;
|
|||||||
|
|
||||||
class SnAttachmentProvider {
|
class SnAttachmentProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
final Map<String, SnAttachment> _cache = {};
|
final Map<String, SnAttachment> _cache = {};
|
||||||
|
|
||||||
SnAttachmentProvider(BuildContext context) {
|
SnAttachmentProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
||||||
@ -28,21 +33,33 @@ class SnAttachmentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
|
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
|
||||||
|
// In-memory cache
|
||||||
if (!noCache && _cache.containsKey(rid)) {
|
if (!noCache && _cache.containsKey(rid)) {
|
||||||
return _cache[rid]!;
|
return _cache[rid]!;
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
final dbResp = await (_dt.db.snLocalAttachment.select()
|
||||||
|
..where((e) => e.rid.equals(rid))
|
||||||
|
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (dbResp != null) {
|
||||||
|
_cache[rid] = dbResp.content;
|
||||||
|
return dbResp.content;
|
||||||
|
}
|
||||||
|
// Remote server
|
||||||
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
||||||
final out = SnAttachment.fromJson(resp.data);
|
final out = SnAttachment.fromJson(resp.data);
|
||||||
if (out.isAnalyzed) {
|
if (out.isAnalyzed) {
|
||||||
_cache[rid] = out;
|
_cache[rid] = out;
|
||||||
}
|
}
|
||||||
|
_saveToLocal([out]);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SnAttachment?>> getMultiple(List<String> rids,
|
Future<List<SnAttachment?>> getMultiple(List<String> rids,
|
||||||
{noCache = false}) async {
|
{bool noCache = false}) async {
|
||||||
|
// In-memory cache
|
||||||
final result = List<SnAttachment?>.filled(rids.length, null);
|
final result = List<SnAttachment?>.filled(rids.length, null);
|
||||||
final Map<String, int> randomMapping = {};
|
final Map<String, int> randomMapping = {};
|
||||||
for (int i = 0; i < rids.length; i++) {
|
for (int i = 0; i < rids.length; i++) {
|
||||||
@ -53,29 +70,44 @@ class SnAttachmentProvider {
|
|||||||
result[i] = _cache[rid]!;
|
result[i] = _cache[rid]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final pendingFetch = randomMapping.keys;
|
var pendingFetch = randomMapping.keys;
|
||||||
|
// On-disk cache
|
||||||
if (pendingFetch.isNotEmpty) {
|
if (pendingFetch.isEmpty) return result;
|
||||||
final resp = await _sn.client.get(
|
if (!noCache) {
|
||||||
'/cgi/uc/attachments',
|
final dbResp = await (_dt.db.snLocalAttachment.select()
|
||||||
queryParameters: {
|
..where((e) => e.rid.isIn(pendingFetch))
|
||||||
'take': pendingFetch.length,
|
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||||
'id': pendingFetch.join(','),
|
.get();
|
||||||
},
|
for (final item in dbResp) {
|
||||||
);
|
if (item.content.isAnalyzed) {
|
||||||
final List<SnAttachment?> out = resp.data['data']
|
_cache[item.rid] = item.content;
|
||||||
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
|
||||||
.cast<SnAttachment?>()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
for (final item in out) {
|
|
||||||
if (item == null) continue;
|
|
||||||
if (item.isAnalyzed) {
|
|
||||||
_cache[item.rid] = item;
|
|
||||||
}
|
}
|
||||||
result[randomMapping[item.rid]!] = item;
|
result[randomMapping[item.rid]!] = item.content;
|
||||||
|
randomMapping.remove(item.rid);
|
||||||
}
|
}
|
||||||
|
pendingFetch = randomMapping.keys;
|
||||||
}
|
}
|
||||||
|
// Remote server
|
||||||
|
if (pendingFetch.isEmpty) return result;
|
||||||
|
final resp = await _sn.client.get(
|
||||||
|
'/cgi/uc/attachments',
|
||||||
|
queryParameters: {
|
||||||
|
'take': pendingFetch.length,
|
||||||
|
'id': pendingFetch.join(','),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final List<SnAttachment?> out = resp.data['data']
|
||||||
|
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
|
||||||
|
.cast<SnAttachment?>()
|
||||||
|
.toList();
|
||||||
|
for (final item in out) {
|
||||||
|
if (item == null) continue;
|
||||||
|
if (item.isAnalyzed) {
|
||||||
|
_cache[item.rid] = item;
|
||||||
|
}
|
||||||
|
result[randomMapping[item.rid]!] = item;
|
||||||
|
}
|
||||||
|
_saveToLocal(out.where((ele) => ele != null).cast());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -274,6 +306,31 @@ class SnAttachmentProvider {
|
|||||||
'metadata': metadata ?? item.usermeta,
|
'metadata': metadata ?? item.usermeta,
|
||||||
'is_indexable': isIndexable ?? item.isIndexable,
|
'is_indexable': isIndexable ?? item.isIndexable,
|
||||||
});
|
});
|
||||||
return SnAttachment.fromJson(resp.data);
|
final out = SnAttachment.fromJson(resp.data);
|
||||||
|
_saveToLocal([out]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToLocal(Iterable<SnAttachment> out) async {
|
||||||
|
for (final ele in out) {
|
||||||
|
if (!ele.isAnalyzed || ele.destination == 0) continue;
|
||||||
|
await _dt.db.snLocalAttachment.insertOne(
|
||||||
|
SnLocalAttachmentCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
rid: ele.rid,
|
||||||
|
uuid: ele.uuid,
|
||||||
|
content: ele,
|
||||||
|
accountId: ele.accountId,
|
||||||
|
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalAttachmentCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
cacheExpiredAt:
|
||||||
|
Constant(DateTime.now().add(const Duration(days: 7))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/database/database.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
|
|
||||||
class SnStickerProvider {
|
class SnStickerProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
final Map<String, SnSticker?> _cache = {};
|
final Map<String, SnSticker?> _cache = {};
|
||||||
|
|
||||||
final Map<int, List<SnSticker>> stickersByPack = {};
|
final Map<int, List<SnSticker>> stickersByPack = {};
|
||||||
@ -15,6 +21,7 @@ class SnStickerProvider {
|
|||||||
|
|
||||||
SnStickerProvider(BuildContext context) {
|
SnStickerProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasNotSticker(String alias) {
|
bool hasNotSticker(String alias) {
|
||||||
@ -31,22 +38,32 @@ class SnStickerProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void putSticker(Iterable<SnSticker> sticker) {
|
void putSticker(Iterable<SnSticker> stickers) {
|
||||||
for (final ele in sticker) {
|
for (final ele in stickers) {
|
||||||
_cacheSticker(ele);
|
_cacheSticker(ele);
|
||||||
}
|
}
|
||||||
|
_saveStickerToLocal(stickers);
|
||||||
|
_saveStickerPackToLocal(stickers.map((ele) => ele.pack).toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnSticker?> lookupSticker(String alias) async {
|
Future<SnSticker?> lookupSticker(String alias) async {
|
||||||
|
// In-memory cache
|
||||||
if (_cache.containsKey(alias)) {
|
if (_cache.containsKey(alias)) {
|
||||||
return _cache[alias];
|
return _cache[alias];
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
final localStickers = await (_dt.db.snLocalSticker.select()
|
||||||
|
..where((e) => e.fullAlias.equals(alias)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (localStickers != null) {
|
||||||
|
_cache[alias] = localStickers.content;
|
||||||
|
return localStickers.content;
|
||||||
|
}
|
||||||
|
// Remote server
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
|
||||||
final sticker = SnSticker.fromJson(resp.data);
|
final sticker = SnSticker.fromJson(resp.data);
|
||||||
_cacheSticker(sticker);
|
putSticker([sticker]);
|
||||||
|
|
||||||
return sticker;
|
return sticker;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_cache[alias] = null;
|
_cache[alias] = null;
|
||||||
@ -57,6 +74,18 @@ class SnStickerProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> listSticker() async {
|
Future<void> listSticker() async {
|
||||||
|
final localPacks = await _dt.db.snLocalStickerPack.select().get();
|
||||||
|
final localStickers = await _dt.db.snLocalSticker.select().get();
|
||||||
|
final local = localStickers.map((ele) {
|
||||||
|
return ele.content.copyWith(
|
||||||
|
pack: localPacks
|
||||||
|
.firstWhere((pk) => pk.content.id == ele.content.packId)
|
||||||
|
.content,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
for (final sticker in local) {
|
||||||
|
_cacheSticker(sticker);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/uc/stickers');
|
final resp = await _sn.client.get('/cgi/uc/stickers');
|
||||||
final data = resp.data;
|
final data = resp.data;
|
||||||
@ -69,4 +98,35 @@ class SnStickerProvider {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveStickerToLocal(Iterable<SnSticker> stickers) async {
|
||||||
|
await _dt.db.snLocalSticker.insertAll(
|
||||||
|
stickers.map(
|
||||||
|
(ele) => SnLocalStickerCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
alias: ele.alias,
|
||||||
|
fullAlias: '${ele.pack.prefix}${ele.alias}',
|
||||||
|
content: ele,
|
||||||
|
createdAt: Value(ele.createdAt),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onConflict: DoNothing(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveStickerPackToLocal(Iterable<SnStickerPack> packs) async {
|
||||||
|
final queries = packs
|
||||||
|
.map(
|
||||||
|
(ele) => _dt.db.snLocalStickerPack.insertOne(
|
||||||
|
SnLocalStickerPackCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
content: ele,
|
||||||
|
createdAt: Value(ele.createdAt),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate((_) => SnLocalStickerPackCompanion.custom(
|
||||||
|
content: Constant(jsonEncode(ele.toJson()))))),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
await Future.wait(queries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,44 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/database/database.dart';
|
||||||
|
import 'package:surface/providers/database.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
class UserDirectoryProvider {
|
class UserDirectoryProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
UserDirectoryProvider(BuildContext context) {
|
UserDirectoryProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, int> _idCache = {};
|
final Map<String, int> _idCache = {};
|
||||||
final Map<int, SnAccount> _cache = {};
|
final Map<int, SnAccount> _cache = {};
|
||||||
|
DateTime? _cacheExpiredAt;
|
||||||
|
|
||||||
|
Future<int> loadAccountCache({int max = 100}) async {
|
||||||
|
final out = await (_dt.db.snLocalAccount.select()..limit(max)).get();
|
||||||
|
for (final ele in out) {
|
||||||
|
_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 {
|
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 out = List<SnAccount?>.generate(id.length, (e) => null);
|
||||||
final plannedQuery = <int>{};
|
final plannedQuery = <int>{};
|
||||||
for (var idx = 0; idx < out.length; idx++) {
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
@ -27,6 +52,25 @@ class UserDirectoryProvider {
|
|||||||
plannedQuery.add(item);
|
plannedQuery.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
if (plannedQuery.isEmpty) return out;
|
||||||
|
final dbResp = await (_dt.db.snLocalAccount.select()
|
||||||
|
..where((e) => e.id.isIn(plannedQuery))
|
||||||
|
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))
|
||||||
|
..limit(plannedQuery.length))
|
||||||
|
.get();
|
||||||
|
for (var idx = 0; idx < out.length; idx++) {
|
||||||
|
if (out[idx] != null) continue;
|
||||||
|
if (dbResp.length <= idx) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out[idx] = dbResp[idx].content;
|
||||||
|
_cache[dbResp[idx].id] = dbResp[idx].content;
|
||||||
|
_idCache[dbResp[idx].name] = dbResp[idx].id;
|
||||||
|
plannedQuery.remove(dbResp[idx].id);
|
||||||
|
}
|
||||||
|
// Remote server
|
||||||
|
_saveToLocal(out.where((ele) => ele != null).cast());
|
||||||
if (plannedQuery.isEmpty) return out;
|
if (plannedQuery.isEmpty) return out;
|
||||||
final resp = await _sn.client
|
final resp = await _sn.client
|
||||||
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
|
||||||
@ -43,17 +87,29 @@ class UserDirectoryProvider {
|
|||||||
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
|
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
|
||||||
sideIdx++;
|
sideIdx++;
|
||||||
}
|
}
|
||||||
|
if (respDecoded.isNotEmpty) _saveToLocal(respDecoded);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAccount?> getAccount(dynamic id) async {
|
Future<SnAccount?> getAccount(dynamic id) async {
|
||||||
|
// In-memory cache
|
||||||
if (id is String && _idCache.containsKey(id)) {
|
if (id is String && _idCache.containsKey(id)) {
|
||||||
id = _idCache[id];
|
id = _idCache[id];
|
||||||
}
|
}
|
||||||
if (_cache.containsKey(id)) {
|
if (_cache.containsKey(id)) {
|
||||||
return _cache[id];
|
return _cache[id];
|
||||||
}
|
}
|
||||||
|
// On-disk cache
|
||||||
|
final dbResp = await (_dt.db.snLocalAccount.select()
|
||||||
|
..where((e) => e.id.equals(id))
|
||||||
|
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (dbResp != null) {
|
||||||
|
_cache[dbResp.id] = dbResp.content;
|
||||||
|
_idCache[dbResp.name] = dbResp.id;
|
||||||
|
return dbResp.content;
|
||||||
|
}
|
||||||
|
// Remote server
|
||||||
try {
|
try {
|
||||||
final resp = await _sn.client.get('/cgi/id/users/$id');
|
final resp = await _sn.client.get('/cgi/id/users/$id');
|
||||||
final account = SnAccount.fromJson(
|
final account = SnAccount.fromJson(
|
||||||
@ -61,16 +117,42 @@ class UserDirectoryProvider {
|
|||||||
);
|
);
|
||||||
_cache[account.id] = account;
|
_cache[account.id] = account;
|
||||||
if (id is String) _idCache[id] = account.id;
|
if (id is String) _idCache[id] = account.id;
|
||||||
|
_saveToLocal([account]);
|
||||||
return account;
|
return account;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SnAccount? getAccountFromCache(dynamic id) {
|
SnAccount? getFromCache(dynamic id) {
|
||||||
if (id is String && _idCache.containsKey(id)) {
|
if (id is String && _idCache.containsKey(id)) {
|
||||||
id = _idCache[id];
|
id = _idCache[id];
|
||||||
}
|
}
|
||||||
return _cache[id];
|
return _cache[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveToLocal(Iterable<SnAccount> out) async {
|
||||||
|
// For better on conflict resolution
|
||||||
|
// And consider the method usually called with usually small amount of data
|
||||||
|
// Use for to insert each record instead of bulk insert
|
||||||
|
List<Future<int>> queries = out.map((ele) {
|
||||||
|
return _dt.db.snLocalAccount.insertOne(
|
||||||
|
SnLocalAccountCompanion.insert(
|
||||||
|
id: Value(ele.id),
|
||||||
|
name: ele.name,
|
||||||
|
content: ele,
|
||||||
|
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||||
|
),
|
||||||
|
onConflict: DoUpdate(
|
||||||
|
(_) => SnLocalAccountCompanion.custom(
|
||||||
|
name: Constant(ele.name),
|
||||||
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
|
cacheExpiredAt:
|
||||||
|
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
await Future.wait(queries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,9 @@ import 'package:surface/screens/news/news_detail.dart';
|
|||||||
import 'package:surface/screens/news/news_list.dart';
|
import 'package:surface/screens/news/news_list.dart';
|
||||||
import 'package:surface/screens/notification.dart';
|
import 'package:surface/screens/notification.dart';
|
||||||
import 'package:surface/screens/post/post_detail.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_editor.dart';
|
||||||
|
import 'package:surface/screens/post/post_shuffle.dart';
|
||||||
import 'package:surface/screens/post/publisher_page.dart';
|
import 'package:surface/screens/post/publisher_page.dart';
|
||||||
import 'package:surface/screens/post/post_search.dart';
|
import 'package:surface/screens/post/post_search.dart';
|
||||||
import 'package:surface/screens/realm.dart';
|
import 'package:surface/screens/realm.dart';
|
||||||
@ -66,10 +68,15 @@ final _appRoutes = [
|
|||||||
builder: (context, state) => const ExploreScreen(),
|
builder: (context, state) => const ExploreScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/write/:mode',
|
path: '/draft',
|
||||||
|
name: 'postDraftBox',
|
||||||
|
builder: (context, state) => const PostDraftBox(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/write',
|
||||||
name: 'postEditor',
|
name: 'postEditor',
|
||||||
builder: (context, state) => PostEditorScreen(
|
builder: (context, state) => PostEditorScreen(
|
||||||
mode: state.pathParameters['mode']!,
|
mode: state.uri.queryParameters['mode'],
|
||||||
postEditId: int.tryParse(
|
postEditId: int.tryParse(
|
||||||
state.uri.queryParameters['editing'] ?? '',
|
state.uri.queryParameters['editing'] ?? '',
|
||||||
),
|
),
|
||||||
@ -82,6 +89,11 @@ final _appRoutes = [
|
|||||||
extraProps: state.extra as PostEditorExtra?,
|
extraProps: state.extra as PostEditorExtra?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/shuffle',
|
||||||
|
name: 'postShuffle',
|
||||||
|
builder: (context, state) => const PostShuffleScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/search',
|
path: '/search',
|
||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
|
@ -11,7 +11,9 @@ import 'package:surface/providers/database.dart';
|
|||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.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_image.dart';
|
||||||
|
import 'package:surface/widgets/account/account_status.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
@ -112,7 +114,14 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
const Gap(8),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -50,7 +50,8 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.post('/cgi/id/badges/${badge.id}/active');
|
await sn.client.post('/cgi/id/badges/${badge.id}/active');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('badgeActivated'.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
|
context.showSnackbar('badgeActivated'
|
||||||
|
.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
|
||||||
await _fetchBadges();
|
await _fetchBadges();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -90,7 +91,12 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
|
|||||||
title: Text(
|
title: Text(
|
||||||
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
||||||
).tr(),
|
).tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 24,
|
||||||
|
right: 16,
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -18,9 +18,11 @@ import 'package:surface/types/account.dart';
|
|||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/badge.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||||
'company.staff': (
|
'company.staff': (
|
||||||
@ -69,7 +71,8 @@ class UserScreen extends StatefulWidget {
|
|||||||
State<UserScreen> createState() => _UserScreenState();
|
State<UserScreen> createState() => _UserScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
|
class _UserScreenState extends State<UserScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late final ScrollController _scrollController = ScrollController();
|
late final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@ -95,7 +98,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Future<void> _getCheckInRecords() async {
|
Future<void> _getCheckInRecords() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||||
setState(() {
|
setState(() {
|
||||||
_records = List.from(
|
_records = List.from(
|
||||||
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
||||||
@ -128,7 +132,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
|
||||||
_publishers = List<SnPublisher>.from(
|
_publishers = List<SnPublisher>.from(
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@ -174,7 +179,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
'related': _account!.name,
|
'related': _account!.name,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
context.showSnackbar(
|
||||||
|
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -190,9 +196,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
await rel.updateRelationship(
|
||||||
|
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
context.showSnackbar(
|
||||||
|
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -218,12 +226,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
double _appBarBlur = 0.0;
|
double _appBarBlur = 0.0;
|
||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
late final _appBarHeight =
|
||||||
|
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
_appBarBlur =
|
||||||
|
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,18 +301,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _account!.nick,
|
text: _account!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style:
|
||||||
color: Colors.white,
|
Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
shadows: labelShadows,
|
color: Colors.white,
|
||||||
),
|
shadows: labelShadows,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_account!.name}',
|
text: '@${_account!.name}',
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style:
|
||||||
color: Colors.white,
|
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
shadows: labelShadows,
|
color: Colors.white,
|
||||||
),
|
shadows: labelShadows,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@ -311,14 +323,21 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
? Stack(
|
? Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
UniversalImage(
|
if (_account!.banner.isNotEmpty)
|
||||||
sn.getAttachmentUrl(_account!.banner),
|
UniversalImage(
|
||||||
fit: BoxFit.cover,
|
sn.getAttachmentUrl(_account!.banner),
|
||||||
height: imageHeight,
|
fit: BoxFit.cover,
|
||||||
width: _appBarWidth,
|
height: imageHeight,
|
||||||
cacheHeight: imageHeight,
|
width: _appBarWidth,
|
||||||
cacheWidth: _appBarWidth,
|
cacheHeight: imageHeight,
|
||||||
),
|
cacheWidth: _appBarWidth,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHigh,
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -370,7 +389,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@ -420,27 +440,41 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(right: 8),
|
).padding(right: 8),
|
||||||
const Gap(12),
|
if (_account!.profile!.description.isNotEmpty)
|
||||||
Text(_account!.profile!.description).padding(horizontal: 8),
|
const Gap(12)
|
||||||
|
else
|
||||||
|
const Gap(8),
|
||||||
|
if (_account!.profile!.description.isNotEmpty)
|
||||||
|
Text(_account!.profile!.description).padding(horizontal: 8),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Card(
|
Card(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.circle,
|
(_status?.isDisturbable ?? true)
|
||||||
fill: 1,
|
? Symbols.circle
|
||||||
|
: Symbols.do_not_disturb_on,
|
||||||
|
fill: (_status?.isOnline ?? false) ? 1 : 0,
|
||||||
size: 16,
|
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),
|
).padding(all: 4),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
_status != null
|
_status != null
|
||||||
? _status!.isOnline
|
? (_status!.status?.label.isNotEmpty ?? false)
|
||||||
? 'accountStatusOnline'.tr()
|
? _status!.status!.label
|
||||||
: 'accountStatusOffline'.tr()
|
: _status!.isOnline
|
||||||
|
? 'accountStatusOnline'.tr()
|
||||||
|
: 'accountStatusOffline'.tr()
|
||||||
: 'loading'.tr(),
|
: 'loading'.tr(),
|
||||||
),
|
),
|
||||||
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
|
if (_status != null &&
|
||||||
|
!_status!.isOnline &&
|
||||||
|
_status!.lastSeenAt != null)
|
||||||
Text(
|
Text(
|
||||||
'accountStatusLastSeen'.tr(args: [
|
'accountStatusLastSeen'.tr(args: [
|
||||||
_status!.lastSeenAt != null
|
_status!.lastSeenAt != null
|
||||||
@ -457,31 +491,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Wrap(
|
Wrap(
|
||||||
children: _account!.badges
|
children: _account!.badges
|
||||||
.map(
|
.map(
|
||||||
(ele) => Tooltip(
|
(ele) => AccountBadge(badge: ele),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
).padding(horizontal: 8),
|
).padding(horizontal: 8),
|
||||||
@ -493,7 +503,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
|
Text('publisherJoinedAt').tr(args: [
|
||||||
|
DateFormat('y/M/d').format(_account!.createdAt)
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@ -510,6 +522,44 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (_account!.profile!.gender.isNotEmpty ||
|
||||||
|
_account!.profile!.pronouns.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.wc),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
_account!.profile!.gender.isNotEmpty
|
||||||
|
? _account!.profile!.gender
|
||||||
|
: 'unknown'.tr(),
|
||||||
|
),
|
||||||
|
Text(' · ').padding(horizontal: 4),
|
||||||
|
Text(
|
||||||
|
_account!.profile!.pronouns.isNotEmpty
|
||||||
|
? _account!.profile!.pronouns
|
||||||
|
: 'unknown'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_account!.profile!.timeZone.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.schedule),
|
||||||
|
const Gap(8),
|
||||||
|
Text(_account!.profile!.timeZone),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_account!.profile!.location.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.location_on),
|
||||||
|
const Gap(8),
|
||||||
|
Text(_account!.profile!.location),
|
||||||
|
],
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -526,17 +576,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.star),
|
const Icon(Symbols.star),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
Text(
|
||||||
|
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
Text(calcLevelUpProgressLevel(
|
||||||
|
_account?.profile?.experience ?? 0))
|
||||||
|
.fontSize(11)
|
||||||
|
.opacity(0.5),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
constraints: const BoxConstraints(maxWidth: 160),
|
constraints: const BoxConstraints(maxWidth: 160),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
|
value: calcLevelUpProgress(
|
||||||
|
_account?.profile?.experience ?? 0),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
backgroundColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
).alignment(Alignment.centerLeft),
|
).alignment(Alignment.centerLeft),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -546,6 +603,26 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
],
|
],
|
||||||
).padding(all: 16),
|
).padding(all: 16),
|
||||||
),
|
),
|
||||||
|
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||||
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
|
if (_account?.profile?.links.isNotEmpty ?? false)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _account!.profile!.links.entries.map((ele) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Symbols.link),
|
||||||
|
title: Text(ele.key),
|
||||||
|
subtitle: Text(ele.value),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(ele.value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
@ -556,7 +633,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
return Text(
|
return Text(
|
||||||
'accountCheckInNoRecords',
|
'accountCheckInNoRecords',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
|
)
|
||||||
|
.tr()
|
||||||
|
.fontWeight(FontWeight.bold)
|
||||||
|
.center()
|
||||||
|
.padding(horizontal: 20, vertical: 8);
|
||||||
}
|
}
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -573,47 +654,55 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
SliverToBoxAdapter(
|
if (_account?.badges.isNotEmpty ?? false)
|
||||||
child: Column(
|
SliverToBoxAdapter(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
|
children: [
|
||||||
SizedBox(
|
Text('accountBadge')
|
||||||
height: 80,
|
.bold()
|
||||||
width: double.infinity,
|
.fontSize(17)
|
||||||
child: ListView(
|
.tr()
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
.padding(horizontal: 20, bottom: 4),
|
||||||
scrollDirection: Axis.horizontal,
|
SizedBox(
|
||||||
children: [
|
height: 80,
|
||||||
for (final badge in _account?.badges ?? [])
|
width: double.infinity,
|
||||||
SizedBox(
|
child: ListView(
|
||||||
width: 280,
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Card(
|
scrollDirection: Axis.horizontal,
|
||||||
child: ListTile(
|
children: [
|
||||||
leading: Icon(
|
for (final badge in _account?.badges ?? [])
|
||||||
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
|
SizedBox(
|
||||||
color: badge.metadata['color'] != null
|
width: 280,
|
||||||
? HexColor.fromHex(badge.metadata['color']!)
|
child: Card(
|
||||||
: kBadgesMeta[badge.type]?.$3,
|
child: ListTile(
|
||||||
fill: 1,
|
leading: Icon(
|
||||||
|
kBadgesMeta[badge.type]?.$2 ??
|
||||||
|
Symbols.question_mark,
|
||||||
|
color: badge.metadata['color'] != null
|
||||||
|
? HexColor.fromHex(
|
||||||
|
badge.metadata['color']!)
|
||||||
|
: kBadgesMeta[badge.type]?.$3,
|
||||||
|
fill: 1,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
||||||
|
).tr(),
|
||||||
|
subtitle: badge.metadata['title'] != null
|
||||||
|
? Text(badge.metadata['title'])
|
||||||
|
: Text(
|
||||||
|
DateFormat('y/M/d')
|
||||||
|
.format(badge.createdAt),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
|
||||||
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
|
|
||||||
).tr(),
|
|
||||||
subtitle: badge.metadata['title'] != null
|
|
||||||
? Text(badge.metadata['title'])
|
|
||||||
: Text(
|
|
||||||
DateFormat('y/M/d').format(badge.createdAt),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SliverGap(8),
|
const SliverGap(8),
|
||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
@ -699,7 +788,8 @@ class CheckInRecordChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
|
getTooltipColor: (_) =>
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
titlesData: FlTitlesData(
|
titlesData: FlTitlesData(
|
||||||
|
@ -204,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
),
|
||||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.close, size: 28),
|
child: const Icon(Symbols.close, size: 28),
|
||||||
@ -213,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -336,7 +334,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
final title = otherMember != null
|
final title = otherMember != null
|
||||||
? ud.getAccountFromCache(otherMember.accountId)?.nick ?? channel.name
|
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
||||||
: channel.name;
|
: channel.name;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@ -354,10 +352,9 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
Badge(
|
Badge(
|
||||||
label: Text(ud
|
label: Text(
|
||||||
.getAccountFromCache(lastMessage!.sender.accountId)
|
ud.getFromCache(lastMessage!.sender.accountId)?.nick ??
|
||||||
?.nick ??
|
'unknown'.tr()),
|
||||||
'unknown'.tr()),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
@ -400,7 +397,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: otherMember != null
|
content: otherMember != null
|
||||||
? ud.getAccountFromCache(otherMember.accountId)?.avatar
|
? ud.getFromCache(otherMember.accountId)?.avatar
|
||||||
: channel.realm?.avatar,
|
: channel.realm?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||||
),
|
),
|
||||||
|
@ -57,11 +57,10 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final ct = context.read<ChatChannelProvider>();
|
||||||
final resp =
|
final resp = await ct.getChannelProfile(_channel!);
|
||||||
await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/me');
|
_profile = resp;
|
||||||
_profile = SnChannelMember.fromJson(resp.data);
|
_notifyLevel = resp.notify;
|
||||||
_notifyLevel = _profile!.notify;
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
await ud.getAccount(_profile!.accountId);
|
await ud.getAccount(_profile!.accountId);
|
||||||
@ -103,10 +102,12 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.delete(
|
await sn.client.delete(
|
||||||
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
|
'/cgi/im/channels/${_channel!.realm?.alias ?? 'global'}/${_channel!.alias}/me',
|
||||||
);
|
);
|
||||||
|
await ct.removeLocalChannel(_channel!);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -130,12 +131,15 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
setState(() => _isUpdatingNotifyLevel = true);
|
setState(() => _isUpdatingNotifyLevel = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put(
|
final resp = await sn.client.put(
|
||||||
'/cgi/im/channels/${_channel!.keyPath}/members/me/notify',
|
'/cgi/im/channels/${_channel!.keyPath}/members/me/notify',
|
||||||
data: {'notify_level': value},
|
data: {'notify_level': value},
|
||||||
);
|
);
|
||||||
|
_profile = SnChannelMember.fromJson(resp.data);
|
||||||
_notifyLevel = value;
|
_notifyLevel = value;
|
||||||
|
await ct.updateChannelProfile(_profile!);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('channelNotifyLevelApplied'.tr());
|
context.showSnackbar('channelNotifyLevelApplied'.tr());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -289,15 +293,14 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content:
|
content: ud.getFromCache(_profile!.accountId)?.avatar,
|
||||||
ud.getAccountFromCache(_profile!.accountId)?.avatar,
|
|
||||||
radius: 18,
|
radius: 18,
|
||||||
),
|
),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
title: Text('channelEditProfile').tr(),
|
title: Text('channelEditProfile').tr(),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
(_profile?.nick?.isEmpty ?? true)
|
(_profile?.nick?.isEmpty ?? true)
|
||||||
? ud.getAccountFromCache(_profile!.accountId)!.nick
|
? ud.getFromCache(_profile!.accountId)!.nick
|
||||||
: _profile!.nick!,
|
: _profile!.nick!,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
@ -408,11 +411,14 @@ class _ChannelProfileDetailDialogState
|
|||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final ct = context.read<ChatChannelProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put(
|
final resp = await sn.client.put(
|
||||||
'/cgi/im/channels/${widget.channel.keyPath}/members/me',
|
'/cgi/im/channels/${widget.channel.keyPath}/members/me',
|
||||||
data: {'nick': _nickController.text},
|
data: {'nick': _nickController.text},
|
||||||
);
|
);
|
||||||
|
final out = SnChannelMember.fromJson(resp.data);
|
||||||
|
await ct.updateChannelProfile(out);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -575,11 +581,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getFromCache(member.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ??
|
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
'unknown'.tr(),
|
|
||||||
),
|
),
|
||||||
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
subtitle: Text(member.nick ?? 'unknown'.tr()),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
|
@ -277,8 +277,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_channel?.type == 1
|
_channel?.type == 1
|
||||||
? ud.getAccountFromCache(_otherMember?.accountId)?.nick ??
|
? ud.getFromCache(_otherMember?.accountId)?.nick ?? _channel!.name
|
||||||
_channel!.name
|
|
||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.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:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.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 = {
|
const Map<String, IconData> kCategoryIcons = {
|
||||||
'technology': Symbols.tools_wrench,
|
'technology': Symbols.tools_wrench,
|
||||||
'gaming': Symbols.gamepad,
|
'gaming': Symbols.gamepad,
|
||||||
@ -39,17 +41,17 @@ class ExploreScreen extends StatefulWidget {
|
|||||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
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>
|
class _ExploreScreenState extends State<ExploreScreen>
|
||||||
with SingleTickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
late final TabController _tabController =
|
late TabController _tabController = TabController(
|
||||||
TabController(length: 4, vsync: this);
|
length: kPostChannels.length,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
final _fabKey = GlobalKey<ExpandableFabState>();
|
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);
|
final List<SnPostCategory> _categories = List.empty(growable: true);
|
||||||
|
|
||||||
@ -69,14 +71,68 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearFilter() {
|
final List<SnRealm> _realms = List.empty(growable: true);
|
||||||
_selectedCategory = null;
|
|
||||||
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_fetchCategories();
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_tabListen();
|
||||||
|
_fetchCategories();
|
||||||
|
_fetchRealms();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -86,7 +142,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshPosts() async {
|
Future<void> refreshPosts() async {
|
||||||
await _listKeys[_tabController.index].currentState?.refreshPosts();
|
await _listKey.currentState?.refreshPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -111,7 +167,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
),
|
||||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.close, size: 28),
|
child: const Icon(Symbols.close, size: 28),
|
||||||
@ -120,90 +175,39 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('writePostTypeStory').tr(),
|
Text('writePost').tr(),
|
||||||
const Gap(20),
|
const Gap(20),
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
heroTag: null,
|
heroTag: null,
|
||||||
tooltip: 'writePostTypeStory'.tr(),
|
tooltip: 'writePost'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
GoRouter.of(context).pushNamed('postEditor').then((value) {
|
||||||
'mode': 'stories',
|
|
||||||
}).then((value) {
|
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
refreshPosts();
|
refreshPosts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
},
|
},
|
||||||
child: const Icon(Symbols.post_rounded),
|
child: const Icon(Symbols.edit),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('writePostTypeArticle').tr(),
|
Text('postDraftBox').tr(),
|
||||||
const Gap(20),
|
const Gap(20),
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
heroTag: null,
|
heroTag: null,
|
||||||
tooltip: 'writePostTypeArticle'.tr(),
|
tooltip: 'postDraftBox'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed('postEditor', pathParameters: {
|
GoRouter.of(context).pushNamed('postDraftBox');
|
||||||
'mode': 'articles',
|
|
||||||
}).then((value) {
|
|
||||||
if (value == true) {
|
|
||||||
refreshPosts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
},
|
},
|
||||||
child: const Icon(Symbols.news),
|
child: const Icon(Symbols.box_edit),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -216,25 +220,74 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
leading: AutoAppBarLeading(),
|
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,
|
floating: true,
|
||||||
snap: true,
|
snap: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.category),
|
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: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
_toggleShowCategories();
|
||||||
context: context,
|
|
||||||
builder: (context) => _PostCategoryPickerPopup(
|
|
||||||
categories: _categories,
|
|
||||||
selected: _selectedCategory,
|
|
||||||
),
|
|
||||||
).then((value) {
|
|
||||||
if (value != null && context.mounted) {
|
|
||||||
_selectedCategory = value == false ? null : value;
|
|
||||||
refreshPosts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -246,122 +299,79 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
|
isScrollable: _showCategories,
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: [
|
tabs: _showCategories
|
||||||
Tab(
|
? [
|
||||||
child: Row(
|
for (final category in _categories)
|
||||||
mainAxisSize: MainAxisSize.min,
|
Tab(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Icon(Symbols.globe,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
size: 20,
|
children: [
|
||||||
color: Theme.of(context)
|
Icon(
|
||||||
.appBarTheme
|
kCategoryIcons[category.alias] ??
|
||||||
.foregroundColor),
|
Symbols.question_mark,
|
||||||
const Gap(8),
|
color: Theme.of(context)
|
||||||
Flexible(
|
.appBarTheme
|
||||||
child: Text(
|
.foregroundColor!,
|
||||||
'postChannelGlobal',
|
),
|
||||||
maxLines: 1,
|
const Gap(8),
|
||||||
).tr().textColor(
|
Flexible(
|
||||||
Theme.of(context).appBarTheme.foregroundColor),
|
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(
|
body: _PostListWidget(
|
||||||
controller: _tabController,
|
key: _listKey,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -369,15 +379,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostListWidget extends StatefulWidget {
|
class _PostListWidget extends StatefulWidget {
|
||||||
final String? channel;
|
const _PostListWidget({super.key});
|
||||||
final bool withRealm;
|
|
||||||
final Function onClearFilter;
|
|
||||||
|
|
||||||
const _PostListWidget(
|
|
||||||
{super.key,
|
|
||||||
this.channel,
|
|
||||||
this.withRealm = false,
|
|
||||||
required this.onClearFilter});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_PostListWidget> createState() => _PostListWidgetState();
|
State<_PostListWidget> createState() => _PostListWidgetState();
|
||||||
@ -386,25 +388,13 @@ class _PostListWidget extends StatefulWidget {
|
|||||||
class _PostListWidgetState extends State<_PostListWidget> {
|
class _PostListWidgetState extends State<_PostListWidget> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
final List<SnPost> _posts = List.empty(growable: true);
|
SnRealm? get realm => _selectedRealm;
|
||||||
final List<SnRealm> _realms = List.empty(growable: true);
|
|
||||||
SnRealm? _selectedRealm;
|
|
||||||
int? _postCount;
|
|
||||||
|
|
||||||
Future<void> _fetchRealms() async {
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
try {
|
SnRealm? _selectedRealm;
|
||||||
final rels = context.read<SnRealmProvider>();
|
String? _selectedChannel;
|
||||||
final out = await rels.listAvailableRealms();
|
SnPostCategory? _selectedCategory;
|
||||||
setState(() {
|
int? _postCount;
|
||||||
_realms.addAll(out);
|
|
||||||
_selectedRealm = out.firstOrNull;
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (!mounted) return;
|
|
||||||
context.showErrorDialog(err);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
Future<void> _fetchPosts() async {
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
@ -416,7 +406,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
take: 10,
|
take: 10,
|
||||||
offset: _posts.length,
|
offset: _posts.length,
|
||||||
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
|
categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
|
||||||
channel: widget.channel,
|
channel: _selectedChannel,
|
||||||
realm: _selectedRealm?.alias,
|
realm: _selectedRealm?.alias,
|
||||||
);
|
);
|
||||||
final out = result.$1;
|
final out = result.$1;
|
||||||
@ -429,6 +419,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
if (mounted) setState(() => _isBusy = false);
|
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() {
|
Future<void> refreshPosts() {
|
||||||
_postCount = null;
|
_postCount = null;
|
||||||
_posts.clear();
|
_posts.clear();
|
||||||
@ -438,13 +443,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.withRealm) {
|
_fetchPosts();
|
||||||
_fetchRealms().then((_) {
|
|
||||||
_fetchPosts();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_fetchPosts();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -467,52 +466,13 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.clear),
|
icon: const Icon(Symbols.clear),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.onClearFilter.call();
|
setState(() => _selectedCategory = null);
|
||||||
refreshPosts();
|
refreshPosts();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
padding: const EdgeInsets.only(left: 20, right: 4),
|
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(
|
Expanded(
|
||||||
child: MediaQuery.removePadding(
|
child: MediaQuery.removePadding(
|
||||||
context: context,
|
context: context,
|
||||||
@ -521,6 +481,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||||
onRefresh: () => refreshPosts(),
|
onRefresh: () => refreshPosts(),
|
||||||
child: InfiniteList(
|
child: InfiniteList(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
centerLoading: true,
|
centerLoading: true,
|
||||||
@ -542,18 +503,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Gap(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).padding(top: 8),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostCategoryPickerPopup extends StatelessWidget {
|
class _PostListRealmPopup extends StatelessWidget {
|
||||||
final List<SnPostCategory> categories;
|
final List<SnRealm>? realms;
|
||||||
final SnPostCategory? selected;
|
final Function(SnRealm?) onUpdate;
|
||||||
|
|
||||||
const _PostCategoryPickerPopup({required this.categories, this.selected});
|
const _PostListRealmPopup({
|
||||||
|
required this.realms,
|
||||||
|
required this.onUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -563,62 +527,38 @@ class _PostCategoryPickerPopup extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.category, size: 24),
|
const Icon(Symbols.face, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('postCategory')
|
Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
|
||||||
.tr()
|
.tr(),
|
||||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.clear),
|
leading: const Icon(Symbols.close),
|
||||||
title: Text('postFilterReset').tr(),
|
title: Text('postInGlobal').tr(),
|
||||||
subtitle: Text('postFilterResetDescription').tr(),
|
subtitle: Text('postViewInGlobalDescription').tr(),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context, false);
|
onUpdate.call(null);
|
||||||
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GridView.count(
|
child: ListView.builder(
|
||||||
crossAxisCount: 4,
|
itemCount: realms?.length ?? 0,
|
||||||
shrinkWrap: true,
|
itemBuilder: (context, idx) {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
final realm = realms![idx];
|
||||||
childAspectRatio: 1,
|
return ListTile(
|
||||||
children: categories
|
title: Text(realm.name),
|
||||||
.map(
|
subtitle: Text('@${realm.alias}'),
|
||||||
(ele) => InkWell(
|
leading: AccountImage(content: realm.avatar, radius: 18),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_selectedCategory = ele;
|
onUpdate.call(realm);
|
||||||
Navigator.pop(context, ele);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -546,11 +546,26 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
'+${_todayRecord!.resultExperience} EXP',
|
'+${_todayRecord!.resultExperience} EXP',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
if (_todayRecord!.resultCoin >= 0)
|
if (_todayRecord!.resultCoin > 0)
|
||||||
Text(
|
Text(
|
||||||
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
'+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
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/config.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/sn_network.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/attachment.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/realm.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:surface/widgets/post/post_poll_editor.dart';
|
||||||
import 'package:uuid/uuid.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 {
|
class PostEditorExtra {
|
||||||
final String? text;
|
final String? text;
|
||||||
@ -53,7 +55,7 @@ class PostEditorExtra {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PostEditorScreen extends StatefulWidget {
|
class PostEditorScreen extends StatefulWidget {
|
||||||
final String mode;
|
final String? mode;
|
||||||
final int? postEditId;
|
final int? postEditId;
|
||||||
final int? postReplyId;
|
final int? postReplyId;
|
||||||
final int? postRepostId;
|
final int? postRepostId;
|
||||||
@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget {
|
|||||||
State<PostEditorScreen> createState() => _PostEditorScreenState();
|
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(
|
late final PostWriteController _writeController = PostWriteController(
|
||||||
doLoadFromTemporary: widget.postEditId == null,
|
doLoadFromTemporary: widget.postEditId == null,
|
||||||
);
|
);
|
||||||
@ -133,6 +138,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
],
|
],
|
||||||
scope: HotKeyScope.inapp,
|
scope: HotKeyScope.inapp,
|
||||||
);
|
);
|
||||||
|
final HotKey _saveDraftHotKey = HotKey(
|
||||||
|
key: PhysicalKeyboardKey.keyS,
|
||||||
|
modifiers: [
|
||||||
|
(!kIsWeb && Platform.isMacOS)
|
||||||
|
? HotKeyModifier.meta
|
||||||
|
: HotKeyModifier.control
|
||||||
|
],
|
||||||
|
scope: HotKeyScope.inapp,
|
||||||
|
);
|
||||||
|
|
||||||
void _registerHotKey() {
|
void _registerHotKey() {
|
||||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||||
@ -148,6 +162,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
]);
|
]);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
hotKeyManager.register(_saveDraftHotKey, keyDownHandler: (_) async {
|
||||||
|
if (mounted) {
|
||||||
|
_writeController.sendPost(context);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPublisherPopup() {
|
void _showPublisherPopup() {
|
||||||
@ -209,9 +228,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
_writeController.dispose();
|
_writeController.dispose();
|
||||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||||
hotKeyManager.unregister(_pasteHotKey);
|
hotKeyManager.unregister(_pasteHotKey);
|
||||||
|
hotKeyManager.unregister(_saveDraftHotKey);
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -220,14 +241,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_registerHotKey();
|
_registerHotKey();
|
||||||
if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
|
|
||||||
context.showErrorDialog('Unknown post type');
|
|
||||||
Navigator.pop(context);
|
|
||||||
} else {
|
|
||||||
_writeController.setMode(widget.mode);
|
|
||||||
}
|
|
||||||
_fetchRealms();
|
_fetchRealms();
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
|
if (widget.mode != null) {
|
||||||
|
_writeController.setMode(widget.mode!);
|
||||||
|
}
|
||||||
|
_tabController.addListener(() {
|
||||||
|
if (_tabController.indexIsChanging) {
|
||||||
|
_writeController.setMode(kPostTypeAliases[_tabController.index]);
|
||||||
|
}
|
||||||
|
});
|
||||||
_writeController.fetchRelatedPost(
|
_writeController.fetchRelatedPost(
|
||||||
context,
|
context,
|
||||||
editing: widget.postEditId,
|
editing: widget.postEditId,
|
||||||
@ -255,38 +278,55 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: RichText(
|
title: Text(
|
||||||
textAlign: TextAlign.center,
|
_writeController.title.isNotEmpty
|
||||||
text: TextSpan(children: [
|
? _writeController.title
|
||||||
TextSpan(
|
: 'untitled'.tr(),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
actions: [
|
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(
|
IconButton(
|
||||||
icon: const Icon(Symbols.tune),
|
icon: const Icon(Symbols.tune),
|
||||||
onPressed: _writeController.isBusy ? null : _updateMeta,
|
onPressed: _writeController.isBusy ? null : _updateMeta,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
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(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_writeController.editingPost != null)
|
if (_writeController.editingPost != null &&
|
||||||
|
!_writeController.editingDraft)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 4, bottom: 4, left: 20, right: 20),
|
top: 4, bottom: 4, left: 20, right: 20),
|
||||||
@ -374,7 +414,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: EdgeInsets.only(bottom: 160),
|
padding: EdgeInsets.only(bottom: 160),
|
||||||
child: StyledWidget(switch (_writeController.mode) {
|
child: switch (_writeController.mode) {
|
||||||
'stories' => _PostStoryEditor(
|
'stories' => _PostStoryEditor(
|
||||||
controller: _writeController,
|
controller: _writeController,
|
||||||
onTapPublisher: _showPublisherPopup,
|
onTapPublisher: _showPublisherPopup,
|
||||||
@ -396,8 +436,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
onTapRealm: _showRealmPopup,
|
onTapRealm: _showRealmPopup,
|
||||||
),
|
),
|
||||||
_ => const Placeholder(),
|
_ => const Placeholder(),
|
||||||
})
|
},
|
||||||
.padding(top: 8),
|
|
||||||
),
|
),
|
||||||
if (_writeController.attachments.isNotEmpty ||
|
if (_writeController.attachments.isNotEmpty ||
|
||||||
_writeController.thumbnail != null)
|
_writeController.thumbnail != null)
|
||||||
@ -720,7 +759,7 @@ class _PostStoryEditor extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -969,7 +1008,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -1053,7 +1092,7 @@ class _PostQuestionEditor extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(top: 8),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1154,7 +1193,7 @@ class _PostVideoEditor extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 640),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -51,7 +51,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Future<void> _fetchPublishers() async {
|
Future<void> _fetchPublishers() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/co/publishers?realm=${widget.alias}');
|
||||||
_publishers = List<SnPublisher>.from(
|
_publishers = List<SnPublisher>.from(
|
||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
@ -68,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
Future<void> _fetchChannels() async {
|
Future<void> _fetchChannels() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/im/channels/${widget.alias}/public');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/im/channels/${widget.alias}/public');
|
||||||
_channels = List<SnChannel>.from(
|
_channels = List<SnChannel>.from(
|
||||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||||
);
|
);
|
||||||
@ -98,15 +100,32 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return <Widget>[
|
return <Widget>[
|
||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle:
|
||||||
|
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: SliverAppBar(
|
sliver: SliverAppBar(
|
||||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
|
Tab(
|
||||||
Tab(icon: Icon(Symbols.explore, color: Theme.of(context).appBarTheme.foregroundColor)),
|
icon: Icon(Symbols.home,
|
||||||
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
|
color: Theme.of(context)
|
||||||
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
|
.appBarTheme
|
||||||
|
.foregroundColor)),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(Symbols.explore,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor)),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(Symbols.group,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor)),
|
||||||
|
Tab(
|
||||||
|
icon: Icon(Symbols.settings,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.appBarTheme
|
||||||
|
.foregroundColor)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -115,7 +134,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
},
|
},
|
||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
_RealmDetailHomeWidget(realm: _realm, publishers: _publishers, channels: _channels),
|
_RealmDetailHomeWidget(
|
||||||
|
realm: _realm, publishers: _publishers, channels: _channels),
|
||||||
_RealmPostListWidget(realm: _realm),
|
_RealmPostListWidget(realm: _realm),
|
||||||
_RealmMemberListWidget(realm: _realm),
|
_RealmMemberListWidget(realm: _realm),
|
||||||
_RealmSettingsWidget(
|
_RealmSettingsWidget(
|
||||||
@ -137,7 +157,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
final List<SnChannel>? channels;
|
final List<SnChannel>? channels;
|
||||||
|
|
||||||
const _RealmDetailHomeWidget({required this.realm, this.publishers, this.channels});
|
const _RealmDetailHomeWidget(
|
||||||
|
{required this.realm, this.publishers, this.channels});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -168,7 +189,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Text('realmCommunityPublishersHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
child: Text('realmCommunityPublishersHint'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium)
|
||||||
.padding(horizontal: 24, vertical: 8),
|
.padding(horizontal: 24, vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -199,7 +221,8 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
child: Text('realmCommunityPublicChannelsHint'.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium)
|
||||||
.padding(horizontal: 24, vertical: 8),
|
.padding(horizontal: 24, vertical: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -323,10 +346,12 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
try {
|
try {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/realms/${widget.realm!.alias}/members', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
'take': 10,
|
'/cgi/id/realms/${widget.realm!.alias}/members',
|
||||||
'offset': _members.length,
|
queryParameters: {
|
||||||
});
|
'take': 10,
|
||||||
|
'offset': _members.length,
|
||||||
|
});
|
||||||
|
|
||||||
final out = List<SnRealmMember>.from(
|
final out = List<SnRealmMember>.from(
|
||||||
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
|
resp.data['data']?.map((e) => SnRealmMember.fromJson(e)) ?? [],
|
||||||
@ -432,14 +457,14 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(member.accountId)?.avatar,
|
content: ud.getFromCache(member.accountId)?.avatar,
|
||||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
ud.getFromCache(member.accountId)?.nick ?? 'unknown'.tr(),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
ud.getFromCache(member.accountId)?.name ?? 'unknown'.tr(),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Symbols.person_remove),
|
icon: const Icon(Symbols.person_remove),
|
||||||
|
@ -51,26 +51,35 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
leading: Icon(Icons.post_add),
|
leading: Icon(Icons.post_add),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
title: Text('shareIntentPostStory').tr(),
|
title: Text('shareIntentPostStory').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {
|
queryParameters: {
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
extra: PostEditorExtra(
|
extra: PostEditorExtra(
|
||||||
text: value
|
text: value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [
|
||||||
|
SharedMediaType.text,
|
||||||
|
SharedMediaType.url
|
||||||
|
].contains(e.type))
|
||||||
.map((e) => e.path)
|
.map((e) => e.path)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
attachments: value
|
attachments: value
|
||||||
.where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image]
|
.where((e) => [
|
||||||
.contains(e.type))
|
SharedMediaType.video,
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
SharedMediaType.file,
|
||||||
|
SharedMediaType.image
|
||||||
|
].contains(e.type))
|
||||||
|
.map((e) =>
|
||||||
|
PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -78,15 +87,18 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
leading: Icon(Icons.chat_outlined),
|
leading: Icon(Icons.chat_outlined),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
title: Text('shareIntentSendChannel').tr(),
|
title: Text('shareIntentSendChannel').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _ShareIntentChannelSelect(value: value),
|
builder: (context) =>
|
||||||
|
_ShareIntentChannelSelect(value: value),
|
||||||
).then((val) {
|
).then((val) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (val == true) Navigator.pop(context);
|
if (val == true) Navigator.pop(context);
|
||||||
@ -110,7 +122,8 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initialize() async {
|
void _initialize() async {
|
||||||
_shareIntentSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
_shareIntentSubscription =
|
||||||
|
ReceiveSharingIntent.instance.getMediaStream().listen((value) {
|
||||||
if (value.isEmpty) return;
|
if (value.isEmpty) return;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_gotoPost(value);
|
_gotoPost(value);
|
||||||
@ -157,7 +170,8 @@ class _ShareIntentChannelSelect extends StatefulWidget {
|
|||||||
const _ShareIntentChannelSelect({required this.value});
|
const _ShareIntentChannelSelect({required this.value});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState();
|
State<_ShareIntentChannelSelect> createState() =>
|
||||||
|
_ShareIntentChannelSelectState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
||||||
@ -178,8 +192,11 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
final lastMessages = await chan.getLastMessages(channels);
|
final lastMessages = await chan.getLastMessages(channels);
|
||||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||||
channels.sort((a, b) {
|
channels.sort((a, b) {
|
||||||
if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) {
|
if (_lastMessages!.containsKey(a.id) &&
|
||||||
return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt);
|
_lastMessages!.containsKey(b.id)) {
|
||||||
|
return _lastMessages![b.id]!
|
||||||
|
.createdAt
|
||||||
|
.compareTo(_lastMessages![a.id]!.createdAt);
|
||||||
}
|
}
|
||||||
if (_lastMessages!.containsKey(a.id)) return -1;
|
if (_lastMessages!.containsKey(a.id)) return -1;
|
||||||
if (_lastMessages!.containsKey(b.id)) return 1;
|
if (_lastMessages!.containsKey(b.id)) return 1;
|
||||||
@ -232,7 +249,9 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.chat, size: 24),
|
const Icon(Symbols.chat, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('shareIntentSendChannel',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
LoadingIndicator(isActive: _isBusy),
|
LoadingIndicator(isActive: _isBusy),
|
||||||
@ -249,29 +268,34 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
final lastMessage = _lastMessages?[channel.id];
|
final lastMessage = _lastMessages?[channel.id];
|
||||||
|
|
||||||
if (channel.type == 1) {
|
if (channel.type == 1) {
|
||||||
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
|
final otherMember =
|
||||||
(ele) => ele?.accountId != ua.user?.id,
|
channel.members?.cast<SnChannelMember?>().firstWhere(
|
||||||
orElse: () => null,
|
(ele) => ele?.accountId != ua.user?.id,
|
||||||
);
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
|
title: Text(
|
||||||
|
ud.getFromCache(otherMember?.accountId)?.nick ??
|
||||||
|
channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'channelDirectMessageDescription'.tr(args: [
|
'channelDirectMessageDescription'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
|
'@${ud.getFromCache(otherMember?.accountId)?.name}',
|
||||||
]),
|
]),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
leading: AccountImage(
|
leading: AccountImage(
|
||||||
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
|
content:
|
||||||
|
ud.getFromCache(otherMember?.accountId)?.avatar,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
@ -291,7 +315,7 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
title: Text(channel.name),
|
title: Text(channel.name),
|
||||||
subtitle: lastMessage != null
|
subtitle: lastMessage != null
|
||||||
? Text(
|
? Text(
|
||||||
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
'${ud.getFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
@ -316,13 +340,20 @@ class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> {
|
|||||||
},
|
},
|
||||||
extra: ChatRoomScreenExtra(
|
extra: ChatRoomScreenExtra(
|
||||||
initialText: widget.value
|
initialText: widget.value
|
||||||
.where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type))
|
.where((e) => [
|
||||||
|
SharedMediaType.text,
|
||||||
|
SharedMediaType.url
|
||||||
|
].contains(e.type))
|
||||||
.map((e) => e.path)
|
.map((e) => e.path)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
initialAttachments: widget.value
|
initialAttachments: widget.value
|
||||||
.where((e) =>
|
.where((e) => [
|
||||||
[SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type))
|
SharedMediaType.video,
|
||||||
.map((e) => PostWriteMedia.fromFile(XFile(e.path)))
|
SharedMediaType.file,
|
||||||
|
SharedMediaType.image
|
||||||
|
].contains(e.type))
|
||||||
|
.map(
|
||||||
|
(e) => PostWriteMedia.fromFile(XFile(e.path)))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -284,7 +284,10 @@ class _StickerPackAddPopupState extends State<_StickerPackAddPopup> {
|
|||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('stickersAdded'.tr());
|
context.showSnackbar('stickersAdded'.tr());
|
||||||
if (_pack?.stickers != null) stickers.putSticker(_pack!.stickers!);
|
if (_pack?.stickers != null) {
|
||||||
|
stickers.putSticker(
|
||||||
|
_pack!.stickers!.map((ele) => ele.copyWith(pack: _pack!)));
|
||||||
|
}
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
@ -50,16 +50,17 @@ Future<ThemeData> createAppTheme(
|
|||||||
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||||
|
|
||||||
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
|
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
|
||||||
?.split(',')
|
?.split(',')
|
||||||
.map((ele) => ele.trim())
|
.map((ele) => ele.trim())
|
||||||
.toList();
|
.toList() ??
|
||||||
|
['Nunito'];
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: useM3,
|
useMaterial3: useM3,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
fontFamily: inUseFonts?.firstOrNull,
|
fontFamily: inUseFonts.firstOrNull,
|
||||||
fontFamilyFallback: inUseFonts?.sublist(1),
|
fontFamilyFallback: inUseFonts.sublist(1),
|
||||||
iconTheme: IconThemeData(
|
iconTheme: IconThemeData(
|
||||||
fill: 0,
|
fill: 0,
|
||||||
weight: 400,
|
weight: 400,
|
||||||
|
@ -119,13 +119,33 @@ abstract class SnAccountStatusInfo with _$SnAccountStatusInfo {
|
|||||||
required bool isDisturbable,
|
required bool isDisturbable,
|
||||||
required bool isOnline,
|
required bool isOnline,
|
||||||
required DateTime? lastSeenAt,
|
required DateTime? lastSeenAt,
|
||||||
required dynamic status,
|
required SnAccountStatus? status,
|
||||||
}) = _SnAccountStatusInfo;
|
}) = _SnAccountStatusInfo;
|
||||||
|
|
||||||
factory SnAccountStatusInfo.fromJson(Map<String, Object?> json) =>
|
factory SnAccountStatusInfo.fromJson(Map<String, Object?> json) =>
|
||||||
_$SnAccountStatusInfoFromJson(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
|
@freezed
|
||||||
abstract class SnAbuseReport with _$SnAbuseReport {
|
abstract class SnAbuseReport with _$SnAbuseReport {
|
||||||
const factory SnAbuseReport({
|
const factory SnAbuseReport({
|
||||||
|
@ -2139,7 +2139,7 @@ mixin _$SnAccountStatusInfo {
|
|||||||
bool get isDisturbable;
|
bool get isDisturbable;
|
||||||
bool get isOnline;
|
bool get isOnline;
|
||||||
DateTime? get lastSeenAt;
|
DateTime? get lastSeenAt;
|
||||||
dynamic get status;
|
SnAccountStatus? get status;
|
||||||
|
|
||||||
/// Create a copy of SnAccountStatusInfo
|
/// Create a copy of SnAccountStatusInfo
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -2163,13 +2163,13 @@ mixin _$SnAccountStatusInfo {
|
|||||||
other.isOnline == isOnline) &&
|
other.isOnline == isOnline) &&
|
||||||
(identical(other.lastSeenAt, lastSeenAt) ||
|
(identical(other.lastSeenAt, lastSeenAt) ||
|
||||||
other.lastSeenAt == lastSeenAt) &&
|
other.lastSeenAt == lastSeenAt) &&
|
||||||
const DeepCollectionEquality().equals(other.status, status));
|
(identical(other.status, status) || other.status == status));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
|
int get hashCode =>
|
||||||
lastSeenAt, const DeepCollectionEquality().hash(status));
|
Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -2187,7 +2187,9 @@ abstract mixin class $SnAccountStatusInfoCopyWith<$Res> {
|
|||||||
{bool isDisturbable,
|
{bool isDisturbable,
|
||||||
bool isOnline,
|
bool isOnline,
|
||||||
DateTime? lastSeenAt,
|
DateTime? lastSeenAt,
|
||||||
dynamic status});
|
SnAccountStatus? status});
|
||||||
|
|
||||||
|
$SnAccountStatusCopyWith<$Res>? get status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -2224,9 +2226,23 @@ class _$SnAccountStatusInfoCopyWithImpl<$Res>
|
|||||||
status: freezed == status
|
status: freezed == status
|
||||||
? _self.status
|
? _self.status
|
||||||
: status // ignore: cast_nullable_to_non_nullable
|
: 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
|
/// @nodoc
|
||||||
@ -2247,7 +2263,7 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
|
|||||||
@override
|
@override
|
||||||
final DateTime? lastSeenAt;
|
final DateTime? lastSeenAt;
|
||||||
@override
|
@override
|
||||||
final dynamic status;
|
final SnAccountStatus? status;
|
||||||
|
|
||||||
/// Create a copy of SnAccountStatusInfo
|
/// Create a copy of SnAccountStatusInfo
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -2276,13 +2292,13 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
|
|||||||
other.isOnline == isOnline) &&
|
other.isOnline == isOnline) &&
|
||||||
(identical(other.lastSeenAt, lastSeenAt) ||
|
(identical(other.lastSeenAt, lastSeenAt) ||
|
||||||
other.lastSeenAt == lastSeenAt) &&
|
other.lastSeenAt == lastSeenAt) &&
|
||||||
const DeepCollectionEquality().equals(other.status, status));
|
(identical(other.status, status) || other.status == status));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
|
int get hashCode =>
|
||||||
lastSeenAt, const DeepCollectionEquality().hash(status));
|
Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@ -2302,7 +2318,10 @@ abstract mixin class _$SnAccountStatusInfoCopyWith<$Res>
|
|||||||
{bool isDisturbable,
|
{bool isDisturbable,
|
||||||
bool isOnline,
|
bool isOnline,
|
||||||
DateTime? lastSeenAt,
|
DateTime? lastSeenAt,
|
||||||
dynamic status});
|
SnAccountStatus? status});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$SnAccountStatusCopyWith<$Res>? get status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -2339,7 +2358,386 @@ class __$SnAccountStatusInfoCopyWithImpl<$Res>
|
|||||||
status: freezed == status
|
status: freezed == status
|
||||||
? _self.status
|
? _self.status
|
||||||
: status // ignore: cast_nullable_to_non_nullable
|
: 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
|
lastSeenAt: json['last_seen_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['last_seen_at'] as String),
|
: 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(
|
Map<String, dynamic> _$SnAccountStatusInfoToJson(
|
||||||
@ -219,7 +221,41 @@ Map<String, dynamic> _$SnAccountStatusInfoToJson(
|
|||||||
'is_disturbable': instance.isDisturbable,
|
'is_disturbable': instance.isDisturbable,
|
||||||
'is_online': instance.isOnline,
|
'is_online': instance.isOnline,
|
||||||
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
|
'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) =>
|
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
|
||||||
|
@ -25,11 +25,13 @@ abstract class SnCheckInRecord with _$SnCheckInRecord {
|
|||||||
required int resultTier,
|
required int resultTier,
|
||||||
required int resultExperience,
|
required int resultExperience,
|
||||||
required double resultCoin,
|
required double resultCoin,
|
||||||
|
@Default(0) int currentStreak,
|
||||||
required List<int> resultModifiers,
|
required List<int> resultModifiers,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
}) = _SnCheckInRecord;
|
}) = _SnCheckInRecord;
|
||||||
|
|
||||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
|
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnCheckInRecordFromJson(json);
|
||||||
|
|
||||||
String get symbol => kCheckInResultTierSymbols[resultTier];
|
String get symbol => kCheckInResultTierSymbols[resultTier];
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ mixin _$SnCheckInRecord {
|
|||||||
int get resultTier;
|
int get resultTier;
|
||||||
int get resultExperience;
|
int get resultExperience;
|
||||||
double get resultCoin;
|
double get resultCoin;
|
||||||
|
int get currentStreak;
|
||||||
List<int> get resultModifiers;
|
List<int> get resultModifiers;
|
||||||
int get accountId;
|
int get accountId;
|
||||||
|
|
||||||
@ -54,6 +55,8 @@ mixin _$SnCheckInRecord {
|
|||||||
other.resultExperience == resultExperience) &&
|
other.resultExperience == resultExperience) &&
|
||||||
(identical(other.resultCoin, resultCoin) ||
|
(identical(other.resultCoin, resultCoin) ||
|
||||||
other.resultCoin == resultCoin) &&
|
other.resultCoin == resultCoin) &&
|
||||||
|
(identical(other.currentStreak, currentStreak) ||
|
||||||
|
other.currentStreak == currentStreak) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.resultModifiers, resultModifiers) &&
|
.equals(other.resultModifiers, resultModifiers) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@ -71,12 +74,13 @@ mixin _$SnCheckInRecord {
|
|||||||
resultTier,
|
resultTier,
|
||||||
resultExperience,
|
resultExperience,
|
||||||
resultCoin,
|
resultCoin,
|
||||||
|
currentStreak,
|
||||||
const DeepCollectionEquality().hash(resultModifiers),
|
const DeepCollectionEquality().hash(resultModifiers),
|
||||||
accountId);
|
accountId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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 resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
double resultCoin,
|
double resultCoin,
|
||||||
|
int currentStreak,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@ -118,6 +123,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
|
|||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
Object? resultCoin = null,
|
Object? resultCoin = null,
|
||||||
|
Object? currentStreak = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@ -150,6 +156,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
|
|||||||
? _self.resultCoin
|
? _self.resultCoin
|
||||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
as double,
|
as double,
|
||||||
|
currentStreak: null == currentStreak
|
||||||
|
? _self.currentStreak
|
||||||
|
: currentStreak // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _self.resultModifiers
|
? _self.resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
@ -173,6 +183,7 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
required this.resultTier,
|
required this.resultTier,
|
||||||
required this.resultExperience,
|
required this.resultExperience,
|
||||||
required this.resultCoin,
|
required this.resultCoin,
|
||||||
|
this.currentStreak = 0,
|
||||||
required final List<int> resultModifiers,
|
required final List<int> resultModifiers,
|
||||||
required this.accountId})
|
required this.accountId})
|
||||||
: _resultModifiers = resultModifiers,
|
: _resultModifiers = resultModifiers,
|
||||||
@ -194,6 +205,9 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
final int resultExperience;
|
final int resultExperience;
|
||||||
@override
|
@override
|
||||||
final double resultCoin;
|
final double resultCoin;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final int currentStreak;
|
||||||
final List<int> _resultModifiers;
|
final List<int> _resultModifiers;
|
||||||
@override
|
@override
|
||||||
List<int> get resultModifiers {
|
List<int> get resultModifiers {
|
||||||
@ -238,6 +252,8 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
other.resultExperience == resultExperience) &&
|
other.resultExperience == resultExperience) &&
|
||||||
(identical(other.resultCoin, resultCoin) ||
|
(identical(other.resultCoin, resultCoin) ||
|
||||||
other.resultCoin == resultCoin) &&
|
other.resultCoin == resultCoin) &&
|
||||||
|
(identical(other.currentStreak, currentStreak) ||
|
||||||
|
other.currentStreak == currentStreak) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other._resultModifiers, _resultModifiers) &&
|
.equals(other._resultModifiers, _resultModifiers) &&
|
||||||
(identical(other.accountId, accountId) ||
|
(identical(other.accountId, accountId) ||
|
||||||
@ -255,12 +271,13 @@ class _SnCheckInRecord extends SnCheckInRecord {
|
|||||||
resultTier,
|
resultTier,
|
||||||
resultExperience,
|
resultExperience,
|
||||||
resultCoin,
|
resultCoin,
|
||||||
|
currentStreak,
|
||||||
const DeepCollectionEquality().hash(_resultModifiers),
|
const DeepCollectionEquality().hash(_resultModifiers),
|
||||||
accountId);
|
accountId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
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 resultTier,
|
||||||
int resultExperience,
|
int resultExperience,
|
||||||
double resultCoin,
|
double resultCoin,
|
||||||
|
int currentStreak,
|
||||||
List<int> resultModifiers,
|
List<int> resultModifiers,
|
||||||
int accountId});
|
int accountId});
|
||||||
}
|
}
|
||||||
@ -304,6 +322,7 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
|
|||||||
Object? resultTier = null,
|
Object? resultTier = null,
|
||||||
Object? resultExperience = null,
|
Object? resultExperience = null,
|
||||||
Object? resultCoin = null,
|
Object? resultCoin = null,
|
||||||
|
Object? currentStreak = null,
|
||||||
Object? resultModifiers = null,
|
Object? resultModifiers = null,
|
||||||
Object? accountId = null,
|
Object? accountId = null,
|
||||||
}) {
|
}) {
|
||||||
@ -336,6 +355,10 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
|
|||||||
? _self.resultCoin
|
? _self.resultCoin
|
||||||
: resultCoin // ignore: cast_nullable_to_non_nullable
|
: resultCoin // ignore: cast_nullable_to_non_nullable
|
||||||
as double,
|
as double,
|
||||||
|
currentStreak: null == currentStreak
|
||||||
|
? _self.currentStreak
|
||||||
|
: currentStreak // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
resultModifiers: null == resultModifiers
|
resultModifiers: null == resultModifiers
|
||||||
? _self._resultModifiers
|
? _self._resultModifiers
|
||||||
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
: resultModifiers // ignore: cast_nullable_to_non_nullable
|
||||||
|
@ -17,6 +17,7 @@ _SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) =>
|
|||||||
resultTier: (json['result_tier'] as num).toInt(),
|
resultTier: (json['result_tier'] as num).toInt(),
|
||||||
resultExperience: (json['result_experience'] as num).toInt(),
|
resultExperience: (json['result_experience'] as num).toInt(),
|
||||||
resultCoin: (json['result_coin'] as num).toDouble(),
|
resultCoin: (json['result_coin'] as num).toDouble(),
|
||||||
|
currentStreak: (json['current_streak'] as num?)?.toInt() ?? 0,
|
||||||
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
resultModifiers: (json['result_modifiers'] as List<dynamic>)
|
||||||
.map((e) => (e as num).toInt())
|
.map((e) => (e as num).toInt())
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -32,6 +33,7 @@ Map<String, dynamic> _$SnCheckInRecordToJson(_SnCheckInRecord instance) =>
|
|||||||
'result_tier': instance.resultTier,
|
'result_tier': instance.resultTier,
|
||||||
'result_experience': instance.resultExperience,
|
'result_experience': instance.resultExperience,
|
||||||
'result_coin': instance.resultCoin,
|
'result_coin': instance.resultCoin,
|
||||||
|
'current_streak': instance.currentStreak,
|
||||||
'result_modifiers': instance.resultModifiers,
|
'result_modifiers': instance.resultModifiers,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
final double? borderRadius;
|
final double? borderRadius;
|
||||||
final Widget? fallbackWidget;
|
final Widget? fallbackWidget;
|
||||||
final Widget? badge;
|
final Widget? badge;
|
||||||
|
final Offset? badgeOffset;
|
||||||
|
|
||||||
const AccountImage({
|
const AccountImage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -23,6 +24,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
this.fallbackWidget,
|
this.fallbackWidget,
|
||||||
this.badge,
|
this.badge,
|
||||||
|
this.badgeOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,7 +42,8 @@ class AccountImage extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20),
|
borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20),
|
||||||
child: (content?.isEmpty ?? true)
|
child: (content?.isEmpty ?? true)
|
||||||
? Container(
|
? Container(
|
||||||
color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
|
color: backgroundColor ??
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
child: (fallbackWidget ??
|
child: (fallbackWidget ??
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.account_circle,
|
Symbols.account_circle,
|
||||||
@ -58,8 +61,8 @@ class AccountImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (badge != null)
|
if (badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: -4,
|
right: badgeOffset?.dx ?? -4,
|
||||||
bottom: -2,
|
bottom: badgeOffset?.dy ?? -2,
|
||||||
child: badge!,
|
child: badge!,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -8,9 +8,9 @@ import 'package:relative_time/relative_time.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/experience.dart';
|
import 'package:surface/providers/experience.dart';
|
||||||
import 'package:surface/providers/sn_network.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/types/account.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/badge.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
class AccountPopoverCard extends StatelessWidget {
|
class AccountPopoverCard extends StatelessWidget {
|
||||||
@ -72,37 +72,21 @@ class AccountPopoverCard extends StatelessWidget {
|
|||||||
const Gap(8)
|
const Gap(8)
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
if (data.badges.isNotEmpty) const Gap(12),
|
|
||||||
if (data.badges.isNotEmpty)
|
if (data.badges.isNotEmpty)
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: data.badges
|
children: data.badges
|
||||||
.map(
|
.map(
|
||||||
(ele) => Tooltip(
|
(ele) => AccountBadge(badge: ele),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24, bottom: 12, top: 12),
|
||||||
const Gap(8),
|
if (data.profile?.description.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
data.profile?.description ?? '',
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(horizontal: 26, bottom: 8),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -110,7 +94,9 @@ class AccountPopoverCard extends StatelessWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
|
||||||
const Gap(8),
|
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),
|
const Gap(8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -126,25 +112,36 @@ class AccountPopoverCard extends StatelessWidget {
|
|||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
future: sn.client.get('/cgi/id/users/${data.name}/status'),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final SnAccountStatusInfo? status =
|
final SnAccountStatusInfo? status = snapshot.hasData
|
||||||
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
|
? SnAccountStatusInfo.fromJson(snapshot.data!.data)
|
||||||
|
: null;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.circle,
|
(status?.isDisturbable ?? true)
|
||||||
fill: 1,
|
? Symbols.circle
|
||||||
|
: Symbols.do_not_disturb_on,
|
||||||
|
fill: (status?.isOnline ?? false) ? 1 : 0,
|
||||||
size: 16,
|
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),
|
).padding(all: 4),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
status != null
|
status != null
|
||||||
? status.isOnline
|
? (status.status?.label.isNotEmpty ?? false)
|
||||||
? 'accountStatusOnline'.tr()
|
? status.status!.label
|
||||||
: 'accountStatusOffline'.tr()
|
: status.isOnline
|
||||||
|
? 'accountStatusOnline'.tr()
|
||||||
|
: 'accountStatusOffline'.tr()
|
||||||
: 'loading'.tr(),
|
: 'loading'.tr(),
|
||||||
),
|
),
|
||||||
if (status != null && !status.isOnline && status.lastSeenAt != null)
|
if (status != null &&
|
||||||
|
!status.isOnline &&
|
||||||
|
status.lastSeenAt != null)
|
||||||
Text(
|
Text(
|
||||||
'accountStatusLastSeen'.tr(args: [
|
'accountStatusLastSeen'.tr(args: [
|
||||||
status.lastSeenAt != null
|
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 SnAttachment? data;
|
||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
final FilterQuality? filterQuality;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
|
this.filterQuality,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
@ -47,6 +49,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
sn.getAttachmentUrl(data!.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
filterQuality: filterQuality,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
@ -83,13 +86,16 @@ class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
|
||||||
const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false});
|
const _AttachmentItemSensitiveBlur(
|
||||||
|
{required this.child, this.isCompact = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState();
|
State<_AttachmentItemSensitiveBlur> createState() =>
|
||||||
|
_AttachmentItemSensitiveBlurState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> {
|
class _AttachmentItemSensitiveBlurState
|
||||||
|
extends State<_AttachmentItemSensitiveBlur> {
|
||||||
bool _doesShow = false;
|
bool _doesShow = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -124,10 +130,15 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
|
|||||||
Text(
|
Text(
|
||||||
'sensitiveContentDescription',
|
'sensitiveContentDescription',
|
||||||
textAlign: TextAlign.center,
|
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),
|
if (!widget.isCompact) const Gap(16),
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('sensitiveContentReveal').tr().textColor(Colors.white),
|
child: Text('sensitiveContentReveal')
|
||||||
|
.tr()
|
||||||
|
.textColor(Colors.white),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => _doesShow = !_doesShow);
|
setState(() => _doesShow = !_doesShow);
|
||||||
},
|
},
|
||||||
@ -137,7 +148,9 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
|
|||||||
).center(),
|
).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)
|
if (_doesShow)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -174,10 +187,12 @@ class _AttachmentItemContentVideo extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState();
|
State<_AttachmentItemContentVideo> createState() =>
|
||||||
|
_AttachmentItemContentVideoState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
|
class _AttachmentItemContentVideoState
|
||||||
|
extends State<_AttachmentItemContentVideo> {
|
||||||
bool _showContent = false;
|
bool _showContent = false;
|
||||||
bool _showOriginal = false;
|
bool _showOriginal = false;
|
||||||
|
|
||||||
@ -188,7 +203,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
setState(() => _showContent = true);
|
setState(() => _showContent = true);
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
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();
|
_videoPlayer = Player();
|
||||||
_videoController = VideoController(_videoPlayer!);
|
_videoController = VideoController(_videoPlayer!);
|
||||||
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
||||||
@ -201,7 +218,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
_videoPlayer?.open(
|
_videoPlayer?.open(
|
||||||
Media(
|
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,
|
play: true,
|
||||||
);
|
);
|
||||||
@ -283,7 +302,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
Duration(
|
Duration(
|
||||||
milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000,
|
milliseconds:
|
||||||
|
(widget.data.data['duration'] ?? 0).toInt() *
|
||||||
|
1000,
|
||||||
).toString(),
|
).toString(),
|
||||||
style: GoogleFonts.robotoMono(
|
style: GoogleFonts.robotoMono(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -346,7 +367,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
|
|||||||
MaterialDesktopCustomButton(
|
MaterialDesktopCustomButton(
|
||||||
iconSize: 24,
|
iconSize: 24,
|
||||||
onPressed: _toggleOriginal,
|
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(
|
child: Video(
|
||||||
controller: _videoController!,
|
controller: _videoController!,
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
controls:
|
controls: !kIsWeb && (Platform.isAndroid || Platform.isIOS)
|
||||||
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
|
? MaterialVideoControls
|
||||||
|
: MaterialDesktopVideoControls,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -378,10 +402,12 @@ class _AttachmentItemContentAudio extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState();
|
State<_AttachmentItemContentAudio> createState() =>
|
||||||
|
_AttachmentItemContentAudioState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> {
|
class _AttachmentItemContentAudioState
|
||||||
|
extends State<_AttachmentItemContentAudio> {
|
||||||
bool _showContent = false;
|
bool _showContent = false;
|
||||||
|
|
||||||
double? _draggingValue;
|
double? _draggingValue;
|
||||||
@ -552,8 +578,12 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
|
|||||||
overlayShape: SliderComponentShape.noOverlay,
|
overlayShape: SliderComponentShape.noOverlay,
|
||||||
),
|
),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(),
|
secondaryTrackValue: _bufferedPosition
|
||||||
value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(),
|
.inMilliseconds
|
||||||
|
.abs()
|
||||||
|
.toDouble(),
|
||||||
|
value: _draggingValue?.abs() ??
|
||||||
|
_position.inMilliseconds.toDouble().abs(),
|
||||||
min: 0,
|
min: 0,
|
||||||
max: math
|
max: math
|
||||||
.max(
|
.max(
|
||||||
@ -593,7 +623,9 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
|
|||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
IconButton.filled(
|
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: () {
|
onPressed: () {
|
||||||
_audioPlayer!.playOrPause();
|
_audioPlayer!.playOrPause();
|
||||||
},
|
},
|
||||||
|
@ -21,6 +21,7 @@ class AttachmentList extends StatefulWidget {
|
|||||||
final double? minWidth;
|
final double? minWidth;
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
final FilterQuality? filterQuality;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
@ -33,23 +34,27 @@ class AttachmentList extends StatefulWidget {
|
|||||||
this.minWidth,
|
this.minWidth,
|
||||||
this.maxWidth,
|
this.maxWidth,
|
||||||
this.padding,
|
this.padding,
|
||||||
|
this.filterQuality,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
|
static const BorderRadius kDefaultRadius =
|
||||||
|
BorderRadius.all(Radius.circular(8));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttachmentList> createState() => _AttachmentListState();
|
State<AttachmentList> createState() => _AttachmentListState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentListState extends State<AttachmentList> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, layoutConstraints) {
|
builder: (context, layoutConstraints) {
|
||||||
final borderSide =
|
final borderSide = widget.bordered
|
||||||
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
|
? BorderSide(width: 1, color: Theme.of(context).dividerColor)
|
||||||
|
: BorderSide.none;
|
||||||
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: widget.minWidth ?? 80,
|
minWidth: widget.minWidth ?? 80,
|
||||||
@ -58,13 +63,13 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
if (widget.data.length == 1) {
|
if (widget.data.length == 1) {
|
||||||
final singleAspectRatio =
|
final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
|
||||||
widget.data[0]?.data['ratio']?.toDouble() ??
|
|
||||||
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
||||||
'audio' => 16 / 9,
|
'audio' => 16 / 9,
|
||||||
'video' => 16 / 9,
|
'video' => 16 / 9,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}.toDouble();
|
}
|
||||||
|
.toDouble();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: widget.padding ?? EdgeInsets.zero,
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
@ -80,12 +85,18 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
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: () {
|
onTap: () {
|
||||||
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
|
if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
|
||||||
|
return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
@ -100,8 +111,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final fullOfImage =
|
final fullOfImage = widget.data
|
||||||
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
|
.where((ele) => ele?.mediaType == SnMediaType.image)
|
||||||
|
.length ==
|
||||||
|
widget.data.length;
|
||||||
|
|
||||||
if (widget.gridded && fullOfImage) {
|
if (widget.gridded && fullOfImage) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -117,29 +130,38 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
crossAxisCount: math.min(widget.data.length, 2),
|
crossAxisCount: math.min(widget.data.length, 2),
|
||||||
crossAxisSpacing: 4,
|
crossAxisSpacing: 4,
|
||||||
mainAxisSpacing: 4,
|
mainAxisSpacing: 4,
|
||||||
children:
|
children: widget.data
|
||||||
widget.data
|
.mapIndexed(
|
||||||
.mapIndexed(
|
(idx, ele) => GestureDetector(
|
||||||
(idx, ele) => GestureDetector(
|
child: Container(
|
||||||
child: Container(
|
constraints: constraints,
|
||||||
constraints: constraints,
|
child: AttachmentItem(
|
||||||
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
|
data: ele,
|
||||||
),
|
heroTag: heroTags[idx],
|
||||||
onTap: () {
|
fit: BoxFit.cover,
|
||||||
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
filterQuality: widget.filterQuality,
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
|
||||||
initialIndex: idx,
|
|
||||||
heroTags: heroTags,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.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(
|
child: ClipRRect(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
child: Column(
|
child: Column(
|
||||||
children:
|
children: widget.data
|
||||||
widget.data
|
.mapIndexed(
|
||||||
.mapIndexed(
|
(idx, ele) => GestureDetector(
|
||||||
(idx, ele) => GestureDetector(
|
child: AspectRatio(
|
||||||
child: AspectRatio(
|
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
||||||
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
|
child: Container(
|
||||||
child: Container(
|
constraints: constraints,
|
||||||
constraints: constraints,
|
child: AttachmentItem(
|
||||||
child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
|
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(
|
return Container(
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||||
|
width: double.infinity,
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
@ -189,16 +216,22 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
itemCount: widget.data.length,
|
itemCount: widget.data.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
return Container(
|
return Container(
|
||||||
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
|
constraints:
|
||||||
|
constraints.copyWith(maxWidth: widget.maxWidth),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
aspectRatio:
|
||||||
|
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
|
if (widget.data[idx]?.mediaType != SnMediaType.image)
|
||||||
|
return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data:
|
data: widget.data
|
||||||
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
|
.where((ele) =>
|
||||||
|
ele != null &&
|
||||||
|
ele.mediaType == SnMediaType.image)
|
||||||
|
.cast(),
|
||||||
initialIndex: idx,
|
initialIndex: idx,
|
||||||
heroTags: heroTags,
|
heroTags: heroTags,
|
||||||
),
|
),
|
||||||
@ -212,18 +245,25 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
border:
|
||||||
|
Border(top: borderSide, bottom: borderSide),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
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(
|
Positioned(
|
||||||
right: 8,
|
right: 8,
|
||||||
bottom: 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 {
|
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
|
||||||
@override
|
@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:io';
|
||||||
import 'dart:math' show max;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
@ -42,16 +41,20 @@ class AttachmentZoomView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
||||||
late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0);
|
late final PageController _pageController =
|
||||||
|
PageController(initialPage: widget.initialIndex ?? 0);
|
||||||
|
|
||||||
bool _showOverlay = true;
|
bool _showOverlay = true;
|
||||||
bool _dismissable = true;
|
bool _dismissable = true;
|
||||||
|
|
||||||
|
int _page = 0;
|
||||||
|
|
||||||
void _updatePage() {
|
void _updatePage() {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_isCompletedDownload) {
|
if (_isCompletedDownload) {
|
||||||
setState(() => _isCompletedDownload = false);
|
setState(() => _isCompletedDownload = false);
|
||||||
}
|
}
|
||||||
|
_page = _pageController.page?.round() ?? 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +110,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar(
|
context.showSnackbar(
|
||||||
(!kIsWeb && (Platform.isIOS || Platform.isAndroid)) ? 'attachmentSaved'.tr() : 'attachmentSavedDesktop'.tr(),
|
(!kIsWeb && (Platform.isIOS || Platform.isAndroid))
|
||||||
|
? 'attachmentSaved'.tr()
|
||||||
|
: 'attachmentSavedDesktop'.tr(),
|
||||||
action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid))
|
action: (!kIsWeb && (Platform.isIOS || Platform.isAndroid))
|
||||||
? SnackBarAction(
|
? SnackBarAction(
|
||||||
label: 'openInAlbum'.tr(),
|
label: 'openInAlbum'.tr(),
|
||||||
@ -131,7 +136,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
bool _showDetail = false;
|
bool _showDetail = false;
|
||||||
|
|
||||||
@ -150,7 +156,9 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
onDismissed: () {
|
onDismissed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none,
|
direction: _dismissable
|
||||||
|
? DismissiblePageDismissDirection.down
|
||||||
|
: DismissiblePageDismissDirection.none,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@ -165,10 +173,13 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
tag: 'attachment-${widget.data.first.rid}-$heroTag',
|
||||||
child: PhotoView(
|
child: PhotoView(
|
||||||
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
|
key: Key(
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
'attachment-detail-${widget.data.first.rid}-$heroTag'),
|
||||||
|
backgroundDecoration:
|
||||||
|
BoxDecoration(color: Colors.transparent),
|
||||||
scaleStateChangedCallback: (scaleState) {
|
scaleStateChangedCallback: (scaleState) {
|
||||||
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
|
setState(() => _dismissable =
|
||||||
|
scaleState == PhotoViewScaleState.initial);
|
||||||
},
|
},
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.first.rid),
|
sn.getAttachmentUrl(widget.data.first.rid),
|
||||||
@ -181,10 +192,12 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
enableRotation: true,
|
enableRotation: true,
|
||||||
scaleStateChangedCallback: (scaleState) {
|
scaleStateChangedCallback: (scaleState) {
|
||||||
setState(() => _dismissable = scaleState == PhotoViewScaleState.initial);
|
setState(() => _dismissable =
|
||||||
|
scaleState == PhotoViewScaleState.initial);
|
||||||
},
|
},
|
||||||
builder: (context, idx) {
|
builder: (context, idx) {
|
||||||
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
final heroTag =
|
||||||
|
widget.heroTags?.elementAt(idx) ?? uuid.v4();
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: UniversalImage.provider(
|
imageProvider: UniversalImage.provider(
|
||||||
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
|
||||||
@ -200,39 +213,22 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
width: 20.0,
|
width: 20.0,
|
||||||
height: 20.0,
|
height: 20.0,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
|
value: event == null
|
||||||
|
? 0
|
||||||
|
: event.cumulativeBytesLoaded /
|
||||||
|
(event.expectedTotalBytes ?? 1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundDecoration: BoxDecoration(color: Colors.transparent),
|
backgroundDecoration:
|
||||||
|
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(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 300,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.bottomCenter,
|
begin: Alignment.bottomCenter,
|
||||||
@ -255,161 +251,155 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
return Row(
|
||||||
final item = widget.data.elementAt(
|
|
||||||
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
|
|
||||||
);
|
|
||||||
final account = ud.getAccountFromCache(item.accountId);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
if (item.accountId > 0)
|
IconButton(
|
||||||
Row(
|
iconSize: 18,
|
||||||
children: [
|
constraints: const BoxConstraints(),
|
||||||
IgnorePointer(
|
icon: const Icon(Icons.close),
|
||||||
child: AccountImage(
|
style: ButtonStyle(
|
||||||
content: account?.avatar,
|
backgroundColor: MaterialStateProperty.all(
|
||||||
radius: 19,
|
Theme.of(context)
|
||||||
),
|
.colorScheme
|
||||||
),
|
.surface
|
||||||
const Gap(8),
|
.withOpacity(0.5),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Gap(2),
|
IconButton(
|
||||||
IgnorePointer(
|
iconSize: 20,
|
||||||
child: Wrap(
|
constraints: const BoxConstraints(),
|
||||||
spacing: 6,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
visualDensity: VisualDensity.compact,
|
||||||
if (item.metadata['exif'] == null)
|
icon: const Icon(Symbols.hide).padding(all: 6),
|
||||||
Text(
|
onPressed: () {
|
||||||
'#${item.rid}',
|
setState(() => _showOverlay = false);
|
||||||
style: metaTextStyle,
|
}),
|
||||||
),
|
Expanded(
|
||||||
if (item.metadata['exif']?['Model'] != null)
|
child: IgnorePointer(
|
||||||
Text(
|
child: Builder(builder: (context) {
|
||||||
'attachmentShotOn'.tr(args: [
|
final item = widget.data.elementAt(_page);
|
||||||
item.metadata['exif']?['Model'],
|
final doShowCameraInfo =
|
||||||
]),
|
item.metadata['exif']?['Model'] != null;
|
||||||
style: metaTextStyle,
|
final exif = item.metadata['exif'];
|
||||||
).padding(right: 2),
|
return Column(
|
||||||
if (item.metadata['exif']?['Megapixels'] != null &&
|
children: [
|
||||||
item.metadata['exif']?['Model'] != null)
|
if (widget.data.length > 1)
|
||||||
Text(
|
Text(
|
||||||
'${item.metadata['exif']?['Megapixels']}MP',
|
'${_page + 1}/${widget.data.length}',
|
||||||
style: metaTextStyle,
|
style:
|
||||||
)
|
GoogleFonts.robotoMono(fontSize: 13),
|
||||||
else
|
).padding(right: 8),
|
||||||
Text(
|
if (doShowCameraInfo)
|
||||||
item.size.formatBytes(),
|
Text(
|
||||||
style: metaTextStyle,
|
'attachmentShotOn'
|
||||||
),
|
.tr(args: [exif?['Model']]),
|
||||||
if (item.metadata['width'] != null && item.metadata['height'] != null)
|
style: metaTextStyle,
|
||||||
Text(
|
textAlign: TextAlign.center,
|
||||||
'${item.metadata['width']}x${item.metadata['height']}',
|
),
|
||||||
style: metaTextStyle,
|
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),
|
IconButton(
|
||||||
InkWell(
|
constraints: const BoxConstraints(),
|
||||||
onTap: () {
|
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;
|
_showDetail = true;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
builder: (context) => _AttachmentZoomDetailPopup(
|
||||||
data: widget.data
|
data: widget.data.elementAt(_page),
|
||||||
.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_showDetail = false;
|
_showDetail = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(
|
|
||||||
'viewDetailedAttachment'.tr(),
|
|
||||||
style: metaTextStyle.copyWith(decoration: TextDecoration.underline),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
).opacity(_showOverlay ? 1 : 0, animate: true).animate(
|
||||||
.opacity(_showOverlay ? 1 : 0, animate: true)
|
const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (_showOverlay) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _showOverlay = !_showOverlay);
|
setState(() => _showOverlay = !_showOverlay);
|
||||||
},
|
},
|
||||||
onVerticalDragUpdate: (details) {
|
onVerticalDragUpdate: (details) {
|
||||||
if (_showDetail) return;
|
if (_showDetail || !_dismissable) return;
|
||||||
if (details.delta.dy <= -20) {
|
if (details.delta.dy <= -20) {
|
||||||
_showDetail = true;
|
_showDetail = true;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _AttachmentZoomDetailPopup(
|
builder: (context) => _AttachmentZoomDetailPopup(
|
||||||
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
|
data: widget.data.elementAt(_page),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_showDetail = false;
|
_showDetail = false;
|
||||||
@ -429,7 +419,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final account = ud.getAccountFromCache(data.accountId);
|
final account = ud.getFromCache(data.accountId);
|
||||||
|
|
||||||
const tableGap = TableRow(
|
const tableGap = TableRow(
|
||||||
children: [
|
children: [
|
||||||
@ -447,7 +437,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.info, size: 24),
|
const Icon(Symbols.info, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
|
Text('attachmentDetailInfo')
|
||||||
|
.tr()
|
||||||
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -461,7 +453,8 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
TableRow(
|
TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(
|
TableCell(
|
||||||
child: Text('attachmentUploadBy').tr().padding(right: 16),
|
child:
|
||||||
|
Text('attachmentUploadBy').tr().padding(right: 16),
|
||||||
),
|
),
|
||||||
TableCell(
|
TableCell(
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -472,9 +465,13 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
radius: 8,
|
radius: 8,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
|
Text(data.accountId > 0
|
||||||
|
? account?.nick ?? 'unknown'.tr()
|
||||||
|
: 'unknown'.tr()),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
|
Text('#${data.accountId}',
|
||||||
|
style: GoogleFonts.robotoMono())
|
||||||
|
.opacity(0.75),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -495,7 +492,9 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(data.size.formatBytes()),
|
Text(data.size.formatBytes()),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
|
Text('${data.size} Bytes',
|
||||||
|
style: GoogleFonts.robotoMono())
|
||||||
|
.opacity(0.75),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
@ -510,19 +509,27 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
|||||||
TableRow(
|
TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(child: Text('Hash').padding(right: 16)),
|
TableCell(child: Text('Hash').padding(right: 16)),
|
||||||
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
|
TableCell(
|
||||||
|
child: Text(data.hash,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 11))
|
||||||
|
.opacity(0.9)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
tableGap,
|
tableGap,
|
||||||
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
...(data.metadata['exif']?.keys.map((k) => TableRow(
|
||||||
children: [
|
children: [
|
||||||
TableCell(child: Text(k).padding(right: 16)),
|
TableCell(child: Text(k).padding(right: 16)),
|
||||||
TableCell(child: Text(data.metadata['exif'][k].toString())),
|
TableCell(
|
||||||
|
child: Text(
|
||||||
|
data.metadata['exif'][k].toString())),
|
||||||
],
|
],
|
||||||
)) ??
|
)) ??
|
||||||
[]),
|
[]),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom),
|
).padding(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 8,
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -12,10 +12,10 @@ import 'package:surface/providers/config.dart';
|
|||||||
import 'package:surface/providers/keypair.dart';
|
import 'package:surface/providers/keypair.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/screens/account/profile_page.dart';
|
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/account/account_popover.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/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/link_preview.dart';
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
@ -51,7 +51,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final user = ud.getAccountFromCache(data.sender.accountId);
|
final user = ud.getFromCache(data.sender.accountId);
|
||||||
|
|
||||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
||||||
|
|
||||||
@ -109,18 +109,10 @@ class ChatMessage extends StatelessWidget {
|
|||||||
child: AccountImage(
|
child: AccountImage(
|
||||||
content: user?.avatar,
|
content: user?.avatar,
|
||||||
badge: (user?.badges.isNotEmpty ?? false)
|
badge: (user?.badges.isNotEmpty ?? false)
|
||||||
? Icon(
|
? AccountBadge(
|
||||||
kBadgesMeta[user!.badges.first.type]?.$2 ??
|
badge: user!.badges.first,
|
||||||
Symbols.question_mark,
|
radius: 16,
|
||||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
padding: EdgeInsets.all(2),
|
||||||
fill: 1,
|
|
||||||
size: 18,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
offset: Offset(1, 1),
|
|
||||||
blurRadius: 5.0,
|
|
||||||
color: Color.fromARGB(200, 0, 0, 0)),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -214,7 +206,8 @@ class ChatMessage extends StatelessWidget {
|
|||||||
data.type == 'messages.new' &&
|
data.type == 'messages.new' &&
|
||||||
(data.body['text']?.isNotEmpty ?? false) &&
|
(data.body['text']?.isNotEmpty ?? false) &&
|
||||||
(cfg.prefs.getBool(kAppExpandChatLink) ?? true))
|
(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)
|
if (data.preload?.attachments?.isNotEmpty ?? false)
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
|
@ -380,7 +380,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
_isEncrypted ? Icon(Symbols.lock, size: 18) : null,
|
||||||
hintText: widget.otherMember != null
|
hintText: widget.otherMember != null
|
||||||
? 'fieldChatMessageDirect'.tr(args: [
|
? 'fieldChatMessageDirect'.tr(args: [
|
||||||
'@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}',
|
'@${ud.getFromCache(widget.otherMember?.accountId)?.name}',
|
||||||
])
|
])
|
||||||
: 'fieldChatMessage'.tr(args: [
|
: 'fieldChatMessage'.tr(args: [
|
||||||
widget.controller.channel?.name ?? 'loading'.tr()
|
widget.controller.channel?.name ?? 'loading'.tr()
|
||||||
|
@ -33,11 +33,13 @@ class ChatTypingIndicator extends StatelessWidget {
|
|||||||
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
const Icon(Symbols.more_horiz, weight: 600, size: 20),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text(
|
Text(
|
||||||
'messageTyping'.plural(controller.typingMembers.length, args: [
|
'messageTyping'
|
||||||
|
.plural(controller.typingMembers.length, args: [
|
||||||
controller.typingMembers
|
controller.typingMembers
|
||||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
? ele.nick!
|
? ele.nick!
|
||||||
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
|
: ud.getFromCache(ele.accountId)?.name ??
|
||||||
|
'unknown')
|
||||||
.join(', '),
|
.join(', '),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -140,28 +140,14 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
future: st.lookupSticker(alias),
|
future: st.lookupSticker(alias),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return GestureDetector(
|
return UniversalImage(
|
||||||
child: UniversalImage(
|
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
|
||||||
sn.getAttachmentUrl(
|
fit: BoxFit.contain,
|
||||||
snapshot.data!.attachment.rid),
|
width: size,
|
||||||
fit: BoxFit.contain,
|
height: size,
|
||||||
width: size,
|
cacheHeight: size,
|
||||||
height: size,
|
cacheWidth: size,
|
||||||
cacheHeight: size,
|
);
|
||||||
cacheWidth: size,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
if (snapshot.data == null) return;
|
|
||||||
context.pushTransparentRoute(
|
|
||||||
AttachmentZoomView(
|
|
||||||
data: [snapshot.data!.attachment],
|
|
||||||
initialIndex: 0,
|
|
||||||
heroTags: [const Uuid().v4()],
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.black.withOpacity(0.7),
|
|
||||||
rootNavigator: true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
|
@ -29,6 +29,7 @@ import 'package:surface/types/attachment.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/types/reaction.dart';
|
import 'package:surface/types/reaction.dart';
|
||||||
import 'package:surface/widgets/account/account_image.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_item.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/dialog.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:surface/widgets/universal_image.dart';
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
|
|
||||||
|
|
||||||
class OpenablePostItem extends StatelessWidget {
|
class OpenablePostItem extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
final bool showReactions;
|
final bool showReactions;
|
||||||
@ -95,9 +94,10 @@ class OpenablePostItem extends StatelessWidget {
|
|||||||
openColor: Colors.transparent,
|
openColor: Colors.transparent,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
transitionType: ContainerTransitionType.fade,
|
transitionType: ContainerTransitionType.fade,
|
||||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
closedColor:
|
||||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||||
),
|
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||||
|
),
|
||||||
closedShape: const RoundedRectangleBorder(
|
closedShape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
@ -138,15 +138,16 @@ class PostItem extends StatelessWidget {
|
|||||||
final box = context.findRenderObject() as RenderBox?;
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
final url = 'https://solsynth.dev/posts/${data.id}';
|
final url = 'https://solsynth.dev/posts/${data.id}';
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
Share.shareUri(Uri.parse(url),
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
||||||
} else {
|
} else {
|
||||||
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
Share.share(url,
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _doShareViaPicture(BuildContext context) async {
|
void _doShareViaPicture(BuildContext context) async {
|
||||||
final box = context.findRenderObject() as RenderBox?;
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
context.showSnackbar('postSharingViaPicture'.tr());
|
|
||||||
|
|
||||||
final controller = ScreenshotController();
|
final controller = ScreenshotController();
|
||||||
final capturedImage = await controller.captureFromLongWidget(
|
final capturedImage = await controller.captureFromLongWidget(
|
||||||
@ -157,8 +158,9 @@ class PostItem extends StatelessWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
// Create a copy of environments
|
||||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
Provider<UserDirectoryProvider>(create: (_) => context.read()),
|
||||||
],
|
],
|
||||||
child: ResponsiveBreakpoints.builder(
|
child: ResponsiveBreakpoints.builder(
|
||||||
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
|
||||||
@ -186,7 +188,8 @@ class PostItem extends StatelessWidget {
|
|||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
|
await FileSaver.instance.saveFile(
|
||||||
|
name: 'Solar Network Post #${data.id}.png', file: imageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
await imageFile.delete();
|
await imageFile.delete();
|
||||||
@ -200,7 +203,9 @@ class PostItem extends StatelessWidget {
|
|||||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||||
|
|
||||||
// Video full view
|
// Video full view
|
||||||
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
if (showFullPost &&
|
||||||
|
data.type == 'video' &&
|
||||||
|
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -217,10 +222,11 @@ class PostItem extends StatelessWidget {
|
|||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: onSelectAnswer,
|
onSelectAnswer: onSelectAnswer,
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
if (onDeleted != null) {}
|
onDeleted?.call();
|
||||||
},
|
},
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
if (data.preload?.video != null)
|
||||||
|
_PostVideoPlayer(data: data).padding(bottom: 8),
|
||||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||||
_PostFeaturedComment(data: data),
|
_PostFeaturedComment(data: data),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@ -265,10 +271,11 @@ class PostItem extends StatelessWidget {
|
|||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: onSelectAnswer,
|
onSelectAnswer: onSelectAnswer,
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
if (onDeleted != null) {}
|
onDeleted?.call();
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
if (data.preload?.video != null)
|
||||||
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||||
@ -311,8 +318,13 @@ class PostItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
Text('postArticle')
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75)
|
||||||
|
.padding(horizontal: 24, bottom: 8),
|
||||||
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
showComments: showComments,
|
||||||
@ -327,7 +339,8 @@ class PostItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final displayableAttachments = data.preload?.attachments
|
final displayableAttachments = data.preload?.attachments
|
||||||
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
|
?.where((ele) =>
|
||||||
|
ele?.mediaType != SnMediaType.image || data.type != 'article')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@ -349,12 +362,16 @@ class PostItem extends StatelessWidget {
|
|||||||
onShareImage: () => _doShareViaPicture(context),
|
onShareImage: () => _doShareViaPicture(context),
|
||||||
onSelectAnswer: onSelectAnswer,
|
onSelectAnswer: onSelectAnswer,
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
if (onDeleted != null) onDeleted!();
|
onDeleted?.call();
|
||||||
},
|
},
|
||||||
).padding(horizontal: 12, vertical: 8),
|
).padding(horizontal: 12, vertical: 8),
|
||||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
if (data.preload?.video != null)
|
||||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||||
if (data.body['title'] != null || data.body['description'] != null)
|
if (data.type == 'question')
|
||||||
|
_PostQuestionHint(data: data)
|
||||||
|
.padding(horizontal: 16, bottom: 8),
|
||||||
|
if (data.body['title'] != null ||
|
||||||
|
data.body['description'] != null)
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article' && showFullPost,
|
isEnlarge: data.type == 'article' && showFullPost,
|
||||||
@ -368,7 +385,8 @@ class PostItem extends StatelessWidget {
|
|||||||
if (data.repostTo != null)
|
if (data.repostTo != null)
|
||||||
_PostQuoteContent(child: data.repostTo!).padding(
|
_PostQuoteContent(child: data.repostTo!).padding(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
bottom:
|
||||||
|
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
|
||||||
),
|
),
|
||||||
if (data.visibility > 0)
|
if (data.visibility > 0)
|
||||||
_PostVisibilityHint(data: data).padding(
|
_PostVisibilityHint(data: data).padding(
|
||||||
@ -380,7 +398,9 @@ class PostItem extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
|
if (data.tags.isNotEmpty)
|
||||||
|
_PostTagsList(data: data)
|
||||||
|
.padding(horizontal: 16, top: 4, bottom: 6),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -393,12 +413,16 @@ class PostItem extends StatelessWidget {
|
|||||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
if (data.preload?.poll != null)
|
||||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
PostPoll(poll: data.preload!.poll!)
|
||||||
|
.padding(horizontal: 12, vertical: 4),
|
||||||
|
if (data.body['content'] != null &&
|
||||||
|
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
text: data.body['content'],
|
text: data.body['content'],
|
||||||
).padding(horizontal: 4),
|
).padding(horizontal: 4),
|
||||||
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
|
_PostFeaturedComment(data: data, maxWidth: maxWidth)
|
||||||
|
.padding(horizontal: 12),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -460,7 +484,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
if (data.type == 'question')
|
||||||
|
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||||
_PostHeadline(
|
_PostHeadline(
|
||||||
data: data,
|
data: data,
|
||||||
isEnlarge: data.type == 'article',
|
isEnlarge: data.type == 'article',
|
||||||
@ -475,16 +500,20 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
|
if (data.type != 'article' &&
|
||||||
|
(data.preload?.attachments?.isNotEmpty ?? false))
|
||||||
StyledWidget(AttachmentList(
|
StyledWidget(AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
columned: true,
|
columned: true,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
)).padding(horizontal: 16, bottom: 8),
|
)).padding(horizontal: 16, bottom: 8),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
||||||
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
|
if (data.body['content_truncated'] == true)
|
||||||
|
_PostTruncatedHint(data: data),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
@ -544,7 +573,8 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
version: QrVersions.auto,
|
version: QrVersions.auto,
|
||||||
size: 100,
|
size: 100,
|
||||||
gapless: true,
|
gapless: true,
|
||||||
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
|
embeddedImage:
|
||||||
|
AssetImage('assets/icon/icon-light-radius.png'),
|
||||||
embeddedImageStyle: QrEmbeddedImageStyle(
|
embeddedImageStyle: QrEmbeddedImageStyle(
|
||||||
size: Size(28, 28),
|
size: Size(28, 28),
|
||||||
),
|
),
|
||||||
@ -575,9 +605,11 @@ class _PostQuestionHint extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
|
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle,
|
||||||
|
size: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
|
if (data.body['answer'] == null &&
|
||||||
|
data.body['reward']?.toDouble() != null)
|
||||||
Text('postQuestionUnansweredWithReward'.tr(args: [
|
Text('postQuestionUnansweredWithReward'.tr(args: [
|
||||||
'${data.body['reward']}',
|
'${data.body['reward']}',
|
||||||
])).opacity(0.75)
|
])).opacity(0.75)
|
||||||
@ -613,7 +645,9 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
|
||||||
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key
|
? data.metric.reactionList.entries
|
||||||
|
.reduce((a, b) => a.value > b.value ? a : b)
|
||||||
|
.key
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@ -627,7 +661,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
InkWell(
|
InkWell(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
|
if (mostTypicalReaction == null ||
|
||||||
|
kTemplateReactions[mostTypicalReaction] == null)
|
||||||
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
Icon(Symbols.add_reaction, size: 20, color: iconColor)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
@ -639,7 +674,8 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
|
if (data.totalUpvote > 0 &&
|
||||||
|
data.totalUpvote >= data.totalDownvote)
|
||||||
Text('postReactionUpvote').plural(
|
Text('postReactionUpvote').plural(
|
||||||
data.totalUpvote,
|
data.totalUpvote,
|
||||||
)
|
)
|
||||||
@ -658,8 +694,12 @@ class _PostBottomAction extends StatelessWidget {
|
|||||||
data: data,
|
data: data,
|
||||||
onChanged: (value, attr, delta) {
|
onChanged: (value, attr, delta) {
|
||||||
onChanged(data.copyWith(
|
onChanged(data.copyWith(
|
||||||
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
|
totalUpvote: attr == 1
|
||||||
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
|
? data.totalUpvote + delta
|
||||||
|
: data.totalUpvote,
|
||||||
|
totalDownvote: attr == 2
|
||||||
|
? data.totalDownvote + delta
|
||||||
|
: data.totalDownvote,
|
||||||
metric: data.metric.copyWith(reactionList: value),
|
metric: data.metric.copyWith(reactionList: value),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -766,7 +806,9 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'articleWrittenAt'.tr(
|
'articleWrittenAt'.tr(
|
||||||
args: [DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())],
|
args: [
|
||||||
|
DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())
|
||||||
|
],
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
@ -774,7 +816,9 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
if (data.editedAt != null)
|
if (data.editedAt != null)
|
||||||
Text(
|
Text(
|
||||||
'articleEditedAt'.tr(
|
'articleEditedAt'.tr(
|
||||||
args: [DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())],
|
args: [
|
||||||
|
DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())
|
||||||
|
],
|
||||||
),
|
),
|
||||||
style: TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
@ -842,7 +886,7 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
'publisherId': data.publisherId,
|
'publisherId': data.publisherId,
|
||||||
});
|
});
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
|
onDeleted.call();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -871,31 +915,46 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final user = data.publisher.type == 0 ? ud.getAccountFromCache(data.publisher.accountId) : null;
|
final user = data.publisher.type == 0
|
||||||
|
? ud.getFromCache(data.publisher.accountId)
|
||||||
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: AccountImage(
|
child: data.preload?.realm == null
|
||||||
content: data.publisher.avatar,
|
? AccountImage(
|
||||||
radius: isCompact ? 12 : 20,
|
content: data.publisher.avatar,
|
||||||
borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
radius: isCompact ? 12 : 20,
|
||||||
badge: (user?.badges.isNotEmpty ?? false)
|
borderRadius:
|
||||||
? Icon(
|
data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
|
||||||
kBadgesMeta[user!.badges.first.type]?.$2 ?? Symbols.question_mark,
|
badge: (user?.badges.isNotEmpty ?? false)
|
||||||
color: kBadgesMeta[user.badges.first.type]?.$3,
|
? AccountBadge(
|
||||||
fill: 1,
|
badge: user!.badges.first,
|
||||||
size: 18,
|
radius: 16,
|
||||||
shadows: [
|
padding: EdgeInsets.all(2),
|
||||||
Shadow(
|
)
|
||||||
offset: Offset(1, 1),
|
: null,
|
||||||
blurRadius: 5.0,
|
)
|
||||||
color: Color.fromARGB(200, 0, 0, 0),
|
: 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,
|
||||||
),
|
),
|
||||||
],
|
borderRadius: BorderRadius.circular(10),
|
||||||
)
|
),
|
||||||
: null,
|
child: AccountImage(
|
||||||
),
|
content: data.publisher.avatar,
|
||||||
|
radius: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showPopover(
|
showPopover(
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@ -926,8 +985,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
|
? RelativeTime(context).format(
|
||||||
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
|
(data.publishedAt ?? data.createdAt).toLocal())
|
||||||
|
: DateFormat('y/M/d HH:mm').format(
|
||||||
|
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@ -938,15 +999,27 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text('@${data.publisher.name}').fontSize(13),
|
Text('@${data.publisher.name}').fontSize(13),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(
|
Text(
|
||||||
isRelativeDate
|
isRelativeDate
|
||||||
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
|
? RelativeTime(context).format(
|
||||||
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
|
(data.publishedAt ?? data.createdAt).toLocal())
|
||||||
|
: DateFormat('y/M/d HH:mm').format(
|
||||||
|
(data.publishedAt ?? data.createdAt).toLocal()),
|
||||||
).fontSize(13),
|
).fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
@ -986,8 +1059,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': data.typePlural},
|
queryParameters: {
|
||||||
queryParameters: {'editing': data.id.toString()},
|
'editing': data.id.toString(),
|
||||||
|
'mode': data.typePlural,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1014,8 +1089,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': 'stories'},
|
queryParameters: {
|
||||||
queryParameters: {'replying': data.id.toString()},
|
'replying': data.id.toString(),
|
||||||
|
'mode': data.typePlural,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1030,8 +1107,10 @@ class _PostContentHeader extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': 'stories'},
|
queryParameters: {
|
||||||
queryParameters: {'reposting': data.id.toString()},
|
'reposting': data.id.toString(),
|
||||||
|
'mode': 'stories',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1129,7 +1208,8 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
if (data.body['content'] == null) return const SizedBox.shrink();
|
if (data.body['content'] == null) return const SizedBox.shrink();
|
||||||
final content = MarkdownTextContent(
|
final content = MarkdownTextContent(
|
||||||
isAutoWarp: data.type == 'story',
|
isAutoWarp: data.type == 'story',
|
||||||
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
|
isEnlargeSticker:
|
||||||
|
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
|
||||||
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
|
||||||
content: data.body['content'],
|
content: data.body['content'],
|
||||||
attachments: data.preload?.attachments,
|
attachments: data.preload?.attachments,
|
||||||
@ -1178,10 +1258,12 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
onDeleted: () {},
|
onDeleted: () {},
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
_PostContentBody(data: child),
|
_PostContentBody(data: child),
|
||||||
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
|
if (child.visibility > 0)
|
||||||
|
_PostVisibilityHint(data: child).padding(top: 4),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
|
if (child.type != 'article' &&
|
||||||
|
(child.preload?.attachments?.isNotEmpty ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
bottomLeft: Radius.circular(8),
|
bottomLeft: Radius.circular(8),
|
||||||
@ -1332,7 +1414,9 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postReadEstimate').tr(args: [
|
Text('postReadEstimate').tr(args: [
|
||||||
'${Duration(
|
'${Duration(
|
||||||
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
|
seconds: (data.body['content_length'] as num).toDouble() *
|
||||||
|
60 ~/
|
||||||
|
kHumanReadSpeed,
|
||||||
).inSeconds}s',
|
).inSeconds}s',
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
@ -1371,7 +1455,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
// If this is a answered question, fetch the answer instead
|
// If this is a answered question, fetch the answer instead
|
||||||
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
|
final resp =
|
||||||
|
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
|
||||||
_isAnswer = true;
|
_isAnswer = true;
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data));
|
||||||
return;
|
return;
|
||||||
@ -1379,9 +1464,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
|
final resp = await sn.client.get(
|
||||||
'take': 1,
|
'/cgi/co/posts/${widget.data.id}/replies/featured',
|
||||||
});
|
queryParameters: {
|
||||||
|
'take': 1,
|
||||||
|
});
|
||||||
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -1410,7 +1497,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Material(
|
child: Material(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: _isAnswer
|
||||||
|
? Colors.green.withOpacity(0.5)
|
||||||
|
: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -1430,11 +1519,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Gap(2),
|
const Gap(2),
|
||||||
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
|
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
|
||||||
|
size: 20),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text(
|
Text(
|
||||||
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
|
_isAnswer
|
||||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
|
? 'postQuestionAnswerTitle'
|
||||||
|
: 'postFeaturedComment',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium!
|
||||||
|
.copyWith(fontSize: 15),
|
||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -1572,7 +1667,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
|
||||||
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
setState(
|
||||||
|
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -1595,11 +1691,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.book_4_spark, size: 24),
|
const Icon(Symbols.book_4_spark, size: 24),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
|
Text('postGetInsightTitle',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge)
|
||||||
|
.tr(),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
|
Text('postGetInsightDescription',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
.tr()
|
||||||
|
.padding(horizontal: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
if (_response == null)
|
if (_response == null)
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -1617,12 +1718,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
|||||||
leading: const Icon(Symbols.info),
|
leading: const Icon(Symbols.info),
|
||||||
title: Text('aiThinkingProcess'.tr()),
|
title: Text('aiThinkingProcess'.tr()),
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
collapsedBackgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
minTileHeight: 32,
|
minTileHeight: 32,
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
SelectableText(
|
||||||
_thinkingProcess!,
|
_thinkingProcess!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!
|
||||||
|
.copyWith(fontStyle: FontStyle.italic),
|
||||||
).padding(horizontal: 20, vertical: 8),
|
).padding(horizontal: 20, vertical: 8),
|
||||||
],
|
],
|
||||||
).padding(vertical: 8),
|
).padding(vertical: 8),
|
||||||
@ -1659,7 +1764,8 @@ class _PostVideoPlayer extends StatelessWidget {
|
|||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
child: AttachmentItem(
|
||||||
|
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,8 @@ class PostMiniEditor extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostMiniEditorState extends State<PostMiniEditor> {
|
class _PostMiniEditorState extends State<PostMiniEditor> {
|
||||||
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
|
final PostWriteController _writeController =
|
||||||
|
PostWriteController(doLoadFromTemporary: false);
|
||||||
|
|
||||||
bool _isFetching = false;
|
bool _isFetching = false;
|
||||||
|
|
||||||
@ -44,8 +45,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
|
||||||
);
|
);
|
||||||
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
final beforeId = config.prefs.getInt('int_last_publisher_id');
|
||||||
_writeController
|
_writeController.setPublisher(
|
||||||
.setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
|
_publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
|
||||||
|
_publishers?.firstOrNull);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@ -99,11 +101,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
Text(item.nick).textStyle(
|
||||||
|
Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium!),
|
||||||
Text('@${item.name}')
|
Text('@${item.name}')
|
||||||
.textStyle(Theme.of(context).textTheme.bodySmall!)
|
.textStyle(Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!)
|
||||||
.fontSize(12),
|
.fontSize(12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -120,7 +128,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
child: const Icon(Symbols.add),
|
child: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -129,7 +138,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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,
|
value: _writeController.publisher,
|
||||||
onChanged: (SnPublisher? value) {
|
onChanged: (SnPublisher? value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
GoRouter.of(context)
|
||||||
|
.pushNamed('accountPublisherNew')
|
||||||
|
.then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_publishers = null;
|
_publishers = null;
|
||||||
_fetchPublishers();
|
_fetchPublishers();
|
||||||
@ -176,7 +188,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
),
|
),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -185,7 +198,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
TweenAnimationBuilder<double>(
|
TweenAnimationBuilder<double>(
|
||||||
tween: Tween(begin: 0, end: _writeController.progress),
|
tween: Tween(begin: 0, end: _writeController.progress),
|
||||||
duration: Duration(milliseconds: 300),
|
duration: Duration(milliseconds: 300),
|
||||||
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
|
builder: (context, value, _) =>
|
||||||
|
LinearProgressIndicator(value: value, minHeight: 2),
|
||||||
)
|
)
|
||||||
else if (_writeController.isBusy)
|
else if (_writeController.isBusy)
|
||||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||||
@ -200,15 +214,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
pathParameters: {'mode': 'stories'},
|
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
if (widget.postReplyId != null) 'replying': widget.postReplyId.toString(),
|
if (widget.postReplyId != null)
|
||||||
|
'replying': widget.postReplyId.toString(),
|
||||||
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: (_writeController.isBusy || _writeController.publisher == null)
|
onPressed: (_writeController.isBusy ||
|
||||||
|
_writeController.publisher == null)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
_writeController.sendPost(context).then((_) {
|
_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/providers/user_directory.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
|
import 'package:surface/widgets/account/badge.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
|
|
||||||
|
|
||||||
class PublisherPopoverCard extends StatelessWidget {
|
class PublisherPopoverCard extends StatelessWidget {
|
||||||
final SnPublisher data;
|
final SnPublisher data;
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
|
||||||
final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null;
|
final user = data.type == 0 ? ud.getFromCache(data.accountId) : null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -76,37 +75,22 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
const Gap(8)
|
const Gap(8)
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16),
|
).padding(horizontal: 16),
|
||||||
if (user != null && user.badges.isNotEmpty) const Gap(16),
|
|
||||||
if (user != null && user.badges.isNotEmpty)
|
if (user != null && user.badges.isNotEmpty)
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: user.badges
|
children: user.badges
|
||||||
.map(
|
.map(
|
||||||
(ele) => Tooltip(
|
(ele) => AccountBadge(badge: ele),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24, top: 16),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
|
if (data.description.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
data.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).padding(horizontal: 26, bottom: 20),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -146,7 +130,10 @@ class PublisherPopoverCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75),
|
Text('publisherTotalDownvote')
|
||||||
|
.tr()
|
||||||
|
.fontSize(13)
|
||||||
|
.opacity(0.75),
|
||||||
Text(data.totalDownvote.toString()),
|
Text(data.totalDownvote.toString()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -34,11 +34,14 @@ class UniversalImage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
final double? resizeHeight =
|
||||||
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
|
||||||
|
final double? resizeWidth =
|
||||||
|
cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
|
||||||
|
|
||||||
return Image(
|
return Image(
|
||||||
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
|
filterQuality:
|
||||||
|
filterQuality ?? context.read<ConfigProvider>().imageQuality,
|
||||||
image: kIsWeb
|
image: kIsWeb
|
||||||
? UniversalImage.provider(url)
|
? UniversalImage.provider(url)
|
||||||
: ResizeImage(
|
: ResizeImage(
|
||||||
@ -52,7 +55,8 @@ class UniversalImage extends StatelessWidget {
|
|||||||
fit: fit,
|
fit: fit,
|
||||||
loadingBuilder: noProgressIndicator
|
loadingBuilder: noProgressIndicator
|
||||||
? null
|
? null
|
||||||
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
: (BuildContext context, Widget child,
|
||||||
|
ImageChunkEvent? loadingProgress) {
|
||||||
if (loadingProgress == null) return child;
|
if (loadingProgress == null) return child;
|
||||||
return Container(
|
return Container(
|
||||||
constraints: BoxConstraints(maxHeight: 80),
|
constraints: BoxConstraints(maxHeight: 80),
|
||||||
@ -61,12 +65,15 @@ class UniversalImage extends StatelessWidget {
|
|||||||
tween: Tween(
|
tween: Tween(
|
||||||
begin: 0,
|
begin: 0,
|
||||||
end: loadingProgress.expectedTotalBytes != null
|
end: loadingProgress.expectedTotalBytes != null
|
||||||
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
: 0,
|
: 0,
|
||||||
),
|
),
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
builder: (context, value, _) => CircularProgressIndicator(
|
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 BoxFit? fit;
|
||||||
final bool noProgressIndicator;
|
final bool noProgressIndicator;
|
||||||
final bool noErrorWidget;
|
final bool noErrorWidget;
|
||||||
|
final FilterQuality? filterQuality;
|
||||||
|
|
||||||
const AutoResizeUniversalImage(
|
const AutoResizeUniversalImage(
|
||||||
this.url, {
|
this.url, {
|
||||||
@ -123,6 +131,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
|
|||||||
this.fit,
|
this.fit,
|
||||||
this.noProgressIndicator = false,
|
this.noProgressIndicator = false,
|
||||||
this.noErrorWidget = false,
|
this.noErrorWidget = false,
|
||||||
|
this.filterQuality,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -137,6 +146,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
|
|||||||
noErrorWidget: noErrorWidget,
|
noErrorWidget: noErrorWidget,
|
||||||
cacheHeight: constraints.maxHeight,
|
cacheHeight: constraints.maxHeight,
|
||||||
cacheWidth: constraints.maxWidth,
|
cacheWidth: constraints.maxWidth,
|
||||||
|
filterQuality: filterQuality,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
72
pubspec.lock
72
pubspec.lock
@ -173,10 +173,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61"
|
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.9.4"
|
version: "8.9.5"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -301,10 +301,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: croppy
|
name: croppy
|
||||||
sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
|
sha256: "99f4fbb4a4b44d2712e8dcd61c57c1acac151bd53cab11de3babec80407ed266"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.5"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -381,10 +381,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "610739247975c2d0de43482afa13ec1018f63c9fddf97ef3d8dc895faa3b4543"
|
sha256: "306b78788d1bb569edb7c55d622953c2414ca12445b41c9117963e03afc5c513"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.3.2"
|
version: "11.3.3"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -429,18 +429,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: drift
|
name: drift
|
||||||
sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
|
sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.25.1"
|
version: "2.26.0"
|
||||||
drift_dev:
|
drift_dev:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: drift_dev
|
name: drift_dev
|
||||||
sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
|
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.25.2"
|
version: "2.26.0"
|
||||||
drift_flutter:
|
drift_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -710,6 +710,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
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:
|
flutter_colorpicker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -831,10 +839,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_map
|
name: flutter_map
|
||||||
sha256: bbf145e8220531f2f727608c431871c7457f3b134e513543913afd00fdc1cd47
|
sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.0"
|
version: "8.1.1"
|
||||||
flutter_markdown:
|
flutter_markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -871,10 +879,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf"
|
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.26"
|
version: "2.0.27"
|
||||||
flutter_shaders:
|
flutter_shaders:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -937,10 +945,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: a3d6429368603a591ca7c1795799a247998fb213ded509070c2c59708b25df31
|
sha256: a6274c34d61b3d68082f2b0e9a641a3ec197e525d269f35b82f62d5b2c6d9f75
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.3"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1153,10 +1161,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: icons_launcher
|
name: icons_launcher
|
||||||
sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9
|
sha256: "2949eef3d336028d89133f69ef221d877e09deed04ebd8e738ab4a427850a7a2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.1"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1177,10 +1185,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a"
|
sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+21"
|
version: "0.8.12+22"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1409,10 +1417,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: material_symbols_icons
|
name: material_symbols_icons
|
||||||
sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
|
sha256: ca30ccbd97763353bde6bb1076aa4f4d17a40db0804384da77df142102aa225d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2805.1"
|
version: "4.2808.0"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1601,10 +1609,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.15"
|
version: "2.2.16"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1785,10 +1793,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pub_semver
|
name: pub_semver
|
||||||
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
pubspec_parse:
|
pubspec_parse:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1953,10 +1961,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
|
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.6"
|
version: "2.4.8"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2118,10 +2126,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3
|
name: sqlite3
|
||||||
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
|
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.4"
|
version: "2.7.5"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2326,10 +2334,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.14"
|
version: "6.3.15"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
23
pubspec.yaml
23
pubspec.yaml
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.3.2+75
|
version: 2.4.2+78
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -138,6 +138,7 @@ dependencies:
|
|||||||
flutter_map: ^8.1.0
|
flutter_map: ^8.1.0
|
||||||
geolocator: ^13.0.2
|
geolocator: ^13.0.2
|
||||||
fast_rsa: ^3.8.0
|
fast_rsa: ^3.8.0
|
||||||
|
flutter_card_swiper: ^7.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -187,18 +188,14 @@ flutter:
|
|||||||
# "family" key with the font family name, and a "fonts" key with a
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
# list giving the asset and other descriptors for the font. For
|
# list giving the asset and other descriptors for the font. For
|
||||||
# example:
|
# example:
|
||||||
# fonts:
|
fonts:
|
||||||
# - family: Schyler
|
- family: Nunito
|
||||||
# fonts:
|
fonts:
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
- asset: assets/fonts/Nunito-Regular.ttf
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
- asset: assets/fonts/Nunito-Bold.ttf
|
||||||
# style: italic
|
weight: 700
|
||||||
# - family: Trajan Pro
|
- asset: assets/fonts/Nunito-Italic.ttf
|
||||||
# fonts:
|
style: italic
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
# For details regarding fonts from package dependencies,
|
||||||
# see https://flutter.dev/to/font-from-package
|
# see https://flutter.dev/to/font-from-package
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v3.dart' as v3;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@ -14,10 +15,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 3:
|
||||||
|
return v3.DatabaseAtV3(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2];
|
static const versions = const [1, 2, 3];
|
||||||
}
|
}
|
||||||
|
2097
test/drift/my_database/generated/schema_v3.dart
Normal file
2097
test/drift/my_database/generated/schema_v3.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -127,9 +127,29 @@
|
|||||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{{flutter_js}}
|
||||||
|
{{flutter_build_config}}
|
||||||
|
|
||||||
<script src="flutter_bootstrap.js" async=""></script>
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
const renderer = searchParams.get('renderer');
|
||||||
|
let cdn = searchParams.get('cdn');
|
||||||
|
|
||||||
|
if (cdn) {
|
||||||
|
localStorage.setItem('sn-web-canvaskit-cdn', cdn);
|
||||||
|
} else {
|
||||||
|
const storagedCdn = localStorage.getItem('sn-web-canvaskit-cdn');
|
||||||
|
cdn = storagedCdn ?? 'com';
|
||||||
|
}
|
||||||
|
|
||||||
|
_flutter.loader.load({
|
||||||
|
config: {
|
||||||
|
renderer: renderer ?? 'canvaskit',
|
||||||
|
canvasKitVariant: 'full',
|
||||||
|
canvasKitBaseUrl: `https://www.gstatic.${cdn}/flutter-canvaskit/f73bfc4522dd0bc87bbcdb4bb3088082755c5e87`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Reference in New Issue
Block a user