Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e544c0b6c | |||
7d56c5ef31 | |||
c2df1af16d | |||
a8143c6453 | |||
04065061e0 | |||
226eb452e5 | |||
a6715b0872 | |||
43e3404dbb | |||
c91cf7c813 | |||
42ac12b53e | |||
63567bf708 | |||
5d3cadefef | |||
251fbb2503 | |||
0b31d32217 | |||
5ddd4fed2e | |||
48b6d5f6c1 | |||
b83b0b5efb | |||
cb24bd953d | |||
4937dee182 | |||
d612097bb1 | |||
058d668b6b | |||
8b19462c3a | |||
0a381ef09b | |||
9b84e912b2 | |||
b3254e0f2f | |||
f0a3bbe023 | |||
df81c84438 | |||
8b12395fca | |||
cb2b71d194 | |||
7ed508e2bb |
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@ -38,4 +38,27 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output-windows
|
||||
path: build/windows/x64/runner/Release
|
||||
path: build/windows/x64/runner/Release
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
sudo apt-get install libmpv-dev mpv
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
- run: flutter pub get
|
||||
- run: flutter build linux
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output-linux
|
||||
path: build/linux/x64/release/bundle
|
@ -12,9 +12,9 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"alias": "Meltdown",
|
||||
"name": "Meltdown",
|
||||
"attachment_id": "IpDPHEbWDDCbBofX",
|
||||
"pack_id": 4
|
||||
"alias": "BaLoading",
|
||||
"name": "BaLoading",
|
||||
"attachment_id": "2JCI2uh21mKkfk9P",
|
||||
"pack_id": 3
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||
url: {{endpoint}}/cgi/id/dev/notify/122
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
@ -15,12 +15,9 @@ body:json {
|
||||
"client_id": "{{third_client_id}}",
|
||||
"client_secret":"{{third_client_tk}}",
|
||||
"type": "general",
|
||||
"subject": "测试",
|
||||
"subtitle": "Alphabot です",
|
||||
"content": "全新通知动画",
|
||||
"metadata": {
|
||||
"image": "D2EDbcrsTugs3xk5"
|
||||
},
|
||||
"subject": "处理该帐号 @solian 的决定",
|
||||
"subtitle": "违反用户协议",
|
||||
"content": "您的帐号违反了我们用户协议中关于冒充我们官方的行为,至此做出停权的决定。还请见谅。该决定是最终决定,不接受上诉。",
|
||||
"priority": 10
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ body:json {
|
||||
"client_id": "alphabot",
|
||||
"client_secret": "_uR0sVnHTh",
|
||||
"remark": "新年红包",
|
||||
"amount": 9705,
|
||||
"payee_id": 2
|
||||
"amount": 150,
|
||||
"payee_id": 18
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@
|
||||
"screenChatNew": "New Channel",
|
||||
"screenRealm": "Realm",
|
||||
"screenRealmManage": "Edit Realm",
|
||||
"screenRealmDiscovery": "Realm Discovery",
|
||||
"screenRealmNew": "New Realm",
|
||||
"screenNotification": "Notification",
|
||||
"screenPostSearch": "Search Posts",
|
||||
@ -155,6 +156,7 @@
|
||||
"writePostTypeStory": "Post a story",
|
||||
"writePostTypeArticle": "Write an article",
|
||||
"writePostTypeQuestion": "Ask a question",
|
||||
"writePostTypeVideo": "Post a video",
|
||||
"fieldPostPublisher": "Post publisher",
|
||||
"fieldPostContent": "What happened?!",
|
||||
"fieldPostTitle": "Title",
|
||||
@ -617,5 +619,36 @@
|
||||
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||
"postQuestionAnswered": "Answered Question",
|
||||
"postQuestionAnswerSelect": "Select as Answer",
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied."
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||
"postVideoUpload": "Upload Video",
|
||||
"realmJoin": "Join Realm",
|
||||
"realmCommunityHint": "This realm is a community realm, you can freely join.",
|
||||
"realmCommunityPublicChannelsHint": "The public channels in this realm",
|
||||
"realmJoined": "Joined realm {}.",
|
||||
"join": "Join",
|
||||
"pollEditorNew": "New Poll",
|
||||
"pollEditorEdit": "Edit Poll",
|
||||
"pollEditorDelete": "Delete Poll",
|
||||
"pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
|
||||
"pollEditorUnlink": "Unlink Poll",
|
||||
"pollOptionAdd": "Add Option",
|
||||
"pollOptionName": "Option Name",
|
||||
"pollLinkExisting": "Link existing poll",
|
||||
"pollAnswered": "Answered the poll.",
|
||||
"pollVotes": {
|
||||
"one": "{} vote",
|
||||
"other": "{} votes"
|
||||
},
|
||||
"publisherDelete": "Delete Publisher {}",
|
||||
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
|
||||
"channelIsPublic": "Public Channel",
|
||||
"channelIsPublicDescription": "The channel is public, anyone can join.",
|
||||
"channelIsCommunity": "Community Channel",
|
||||
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
|
||||
"realmIsPublic": "Public Realm",
|
||||
"realmIsPublicDescription": "The realm is public, anyone can join.",
|
||||
"realmIsCommunity": "Community Realm",
|
||||
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
|
||||
"realmLeave": "Leave Realm",
|
||||
"realmLeaveDescription": "Leave the current realm and delete the realm's identity."
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天频道",
|
||||
"screenRealm": "领域",
|
||||
"screenRealmManage": "编辑领域",
|
||||
"screenRealmDiscovery": "发现领域",
|
||||
"screenRealmNew": "新建领域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "发动态",
|
||||
"writePostTypeArticle": "写文章",
|
||||
"writePostTypeQuestion": "提问题",
|
||||
"writePostTypeVideo": "发视频",
|
||||
"fieldPostPublisher": "帖子发布者",
|
||||
"fieldPostContent": "发生什么事了?!",
|
||||
"fieldPostTitle": "标题",
|
||||
@ -616,5 +618,36 @@
|
||||
"postQuestionAnswered": "已解答的问题",
|
||||
"postQuestionAnswerTitle": "精选解答",
|
||||
"postQuestionAnswerSelect": "选择解答",
|
||||
"postQuestionAnswerSelected": "解答已选择,奖励已发放。"
|
||||
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
|
||||
"postVideoUpload": "上传视频",
|
||||
"realmJoin": "加入领域",
|
||||
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
|
||||
"realmJoined": "已加入领域 {}。",
|
||||
"join": "加入",
|
||||
"pollEditorNew": "新投票",
|
||||
"pollEditorEdit": "编辑投票",
|
||||
"pollEditorDelete": "删除投票",
|
||||
"pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
|
||||
"pollEditorUnlink": "解除链接",
|
||||
"pollOptionAdd": "添加选项",
|
||||
"pollOptionName": "选项名称",
|
||||
"pollLinkExisting": "链接现有投票",
|
||||
"pollAnswered": "答案已经反馈。",
|
||||
"pollVotes": {
|
||||
"one": "{} 票",
|
||||
"other": "{} 票"
|
||||
},
|
||||
"publisherDelete": "删除发布者 {}",
|
||||
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
|
||||
"channelIsPublic": "公开频道",
|
||||
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
|
||||
"channelIsCommunity": "社区频道",
|
||||
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
|
||||
"realmIsPublic": "公开领域",
|
||||
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
|
||||
"realmIsCommunity": "社区领域",
|
||||
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
|
||||
"realmLeave": "离开领域",
|
||||
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。"
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
@ -616,5 +618,11 @@
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"join": "加入"
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
"screenChatNew": "新建聊天頻道",
|
||||
"screenRealm": "領域",
|
||||
"screenRealmManage": "編輯領域",
|
||||
"screenRealmDiscovery": "發現領域",
|
||||
"screenRealmNew": "新建領域",
|
||||
"screenNotification": "通知",
|
||||
"screenPostSearch": "搜索帖子",
|
||||
@ -139,6 +140,7 @@
|
||||
"writePostTypeStory": "發動態",
|
||||
"writePostTypeArticle": "寫文章",
|
||||
"writePostTypeQuestion": "提問題",
|
||||
"writePostTypeVideo": "發視頻",
|
||||
"fieldPostPublisher": "帖子發佈者",
|
||||
"fieldPostContent": "發生什麼事了?!",
|
||||
"fieldPostTitle": "標題",
|
||||
@ -616,5 +618,11 @@
|
||||
"postQuestionAnswered": "已解答的問題",
|
||||
"postQuestionAnswerTitle": "精選解答",
|
||||
"postQuestionAnswerSelect": "選擇解答",
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
|
||||
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
|
||||
"postVideoUpload": "上傳視頻",
|
||||
"realmJoin": "加入領域",
|
||||
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
|
||||
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
|
||||
"realmJoined": "已加入領域 {}。",
|
||||
"join": "加入"
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- Flutter
|
||||
- device_info_plus (0.0.1):
|
||||
@ -180,7 +179,7 @@ PODS:
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- Kingfisher (8.2.0)
|
||||
- livekit_client (2.3.5):
|
||||
- livekit_client (2.3.6):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -237,7 +236,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
@ -300,7 +299,7 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
:path: ".symlinks/plugins/croppy/ios"
|
||||
device_info_plus:
|
||||
@ -374,7 +373,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
@ -404,7 +403,7 @@ SPEC CHECKSUMS:
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
|
||||
livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
|
@ -16,6 +16,7 @@ import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -145,6 +146,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
'stories': 'writePostTypeStory',
|
||||
'articles': 'writePostTypeArticle',
|
||||
'questions': 'writePostTypeQuestion',
|
||||
'videos': 'writePostTypeVideo',
|
||||
};
|
||||
|
||||
static const kAttachmentProgressWeight = 0.9;
|
||||
@ -197,6 +199,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
PostWriteMedia? thumbnail;
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
SnAttachment? videoAttachment;
|
||||
SnPoll? poll;
|
||||
|
||||
Future<void> fetchRelatedPost(
|
||||
BuildContext context, {
|
||||
@ -218,6 +222,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||
videoAttachment = post.preload?.video;
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -226,6 +231,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
|
||||
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
|
||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
@ -364,6 +370,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
|
||||
if (poll != null) 'poll': poll!.toJson(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -393,6 +400,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
|
||||
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
|
||||
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
|
||||
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
|
||||
temporaryRestored = true;
|
||||
notifyListeners();
|
||||
});
|
||||
@ -507,6 +515,8 @@ class PostWriteController extends ChangeNotifier {
|
||||
if (replyingPost != null) 'reply_to': replyingPost!.id,
|
||||
if (repostingPost != null) 'repost_to': repostingPost!.id,
|
||||
if (reward != null) 'reward': reward,
|
||||
if (videoAttachment != null) 'video': videoAttachment!.rid,
|
||||
if (poll != null) 'poll': poll!.id,
|
||||
},
|
||||
onSendProgress: (count, total) {
|
||||
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
|
||||
@ -633,6 +643,16 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVideoAttachment(SnAttachment? value) {
|
||||
videoAttachment = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setPoll(SnPoll? value) {
|
||||
poll = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
publishedAt = null;
|
||||
publishedUntil = null;
|
||||
|
@ -47,6 +47,8 @@ import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
@ -107,6 +109,13 @@ void main() async {
|
||||
}
|
||||
}
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
|
||||
if (imagePickerImplementation is ImagePickerAndroid) {
|
||||
imagePickerImplementation.useAndroidPhotoPicker = true;
|
||||
}
|
||||
}
|
||||
|
||||
runApp(const SolianApp());
|
||||
}
|
||||
|
||||
@ -160,8 +169,8 @@ class SolianApp extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
breakpoints: [
|
||||
const Breakpoint(start: 0, end: 450, name: MOBILE),
|
||||
const Breakpoint(start: 451, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 0, end: 600, name: MOBILE),
|
||||
const Breakpoint(start: 601, end: 800, name: TABLET),
|
||||
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
|
||||
],
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
|
||||
class SnPostContentProvider {
|
||||
@ -16,6 +17,11 @@ class SnPostContentProvider {
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
}
|
||||
|
||||
Future<SnPoll> _fetchPoll(int id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||
return SnPoll.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||
Set<String> rids = {};
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
@ -23,6 +29,9 @@ class SnPostContentProvider {
|
||||
if (out[i].body['thumbnail'] != null) {
|
||||
rids.add(out[i].body['thumbnail']);
|
||||
}
|
||||
if (out[i].body['video'] != null) {
|
||||
rids.add(out[i].body['video']);
|
||||
}
|
||||
if (out[i].repostTo != null) {
|
||||
out[i] = out[i].copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||
@ -32,10 +41,17 @@ class SnPostContentProvider {
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
SnPoll? poll;
|
||||
if (out[i].pollId != null) {
|
||||
poll = await _fetchPoll(out[i].pollId!);
|
||||
}
|
||||
|
||||
out[i] = out[i].copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -53,6 +69,9 @@ class SnPostContentProvider {
|
||||
if (out.body['thumbnail'] != null) {
|
||||
rids.add(out.body['thumbnail']);
|
||||
}
|
||||
if (out.body['video'] != null) {
|
||||
rids.add(out.body['video']);
|
||||
}
|
||||
if (out.repostTo != null) {
|
||||
out = out.copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||
@ -60,10 +79,18 @@ class SnPostContentProvider {
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
|
||||
SnPoll? poll;
|
||||
if (out.pollId != null) {
|
||||
poll = await _fetchPoll(out.pollId!);
|
||||
}
|
||||
|
||||
out = out.copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
|
||||
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
|
||||
video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
|
||||
poll: poll,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -42,22 +42,22 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
_connectCompleter = null;
|
||||
}
|
||||
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
if (!_ua.isAuthorized) return;
|
||||
if (isConnected || conn != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_connectCompleter = Completer<void>();
|
||||
|
||||
final atk = await _sn.getFreshAtk();
|
||||
final uri = Uri.parse(
|
||||
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
|
||||
);
|
||||
|
||||
isBusy = true;
|
||||
notifyListeners();
|
||||
|
||||
conn = WebSocketChannel.connect(uri);
|
||||
await conn!.ready;
|
||||
_wsStream = conn!.stream.asBroadcastStream();
|
||||
@ -82,6 +82,7 @@ class WebSocketProvider extends ChangeNotifier {
|
||||
isBusy = false;
|
||||
notifyListeners();
|
||||
_connectCompleter!.complete();
|
||||
_connectCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||
import 'package:surface/screens/settings.dart';
|
||||
import 'package:surface/screens/sharing.dart';
|
||||
import 'package:surface/screens/wallet.dart';
|
||||
@ -192,11 +193,6 @@ final _appRoutes = [
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
@ -204,6 +200,16 @@ final _appRoutes = [
|
||||
editingRealmAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/discovery',
|
||||
name: 'realmDiscovery',
|
||||
builder: (context, state) => const RealmDiscoveryScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:alias',
|
||||
name: 'realmDetail',
|
||||
builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
|
||||
|
@ -74,7 +74,10 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (_isBusy)
|
||||
const CircularProgressIndicator().padding(all: 24).center()
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center()
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
@ -178,6 +178,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountPublisherEdit').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
|
@ -45,6 +45,33 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePublisher(SnPublisher publisher) async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'publisherDelete'.tr(args: ['#${publisher.name}']),
|
||||
'publisherDeleteDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
await context
|
||||
.read<SnNetworkProvider>()
|
||||
.client
|
||||
.delete('/cgi/co/publishers/${publisher.name}');
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('publisherDeleted'.tr(args: ['#${publisher.name}']));
|
||||
_publishers.remove(publisher);
|
||||
_fetchPublishers();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -118,6 +145,18 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete),
|
||||
const Gap(16),
|
||||
Text('delete').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_deletePublisher(publisher);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -123,8 +123,10 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
),
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
const CircularProgressIndicator().padding(all: 24).center(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: const CircularProgressIndicator(),
|
||||
).center(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatManageScreen extends StatefulWidget {
|
||||
final String? editingChannelAlias;
|
||||
|
||||
const ChatManageScreen({super.key, this.editingChannelAlias});
|
||||
|
||||
@override
|
||||
@ -33,6 +35,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
SnRealm? _belongToRealm;
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
@ -41,6 +48,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
if (_editingChannel != null) {
|
||||
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
} finally {
|
||||
@ -48,8 +58,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
SnChannel? _editingChannel;
|
||||
|
||||
Future<void> _fetchChannel() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -62,6 +70,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
_aliasController.text = _editingChannel!.alias;
|
||||
_nameController.text = _editingChannel!.name;
|
||||
_descriptionController.text = _editingChannel!.description;
|
||||
_isPublic = _editingChannel!.isPublic;
|
||||
_isCommunity = _editingChannel!.isCommunity;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -83,6 +93,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
: uuid.v4().replaceAll('-', '').substring(0, 12),
|
||||
'name': _nameController.text,
|
||||
'description': _descriptionController.text,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -124,9 +136,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
: Text('screenChatNew').tr(),
|
||||
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
@ -138,8 +148,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text(
|
||||
'channelEditingNotice'
|
||||
.tr(args: ['#${_editingChannel!.alias}']),
|
||||
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -162,6 +171,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
items: [
|
||||
...(_realms?.map(
|
||||
(SnRealm item) => DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
|
||||
value: item,
|
||||
child: Row(
|
||||
children: [
|
||||
@ -179,15 +189,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name).textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).textStyle(
|
||||
Theme.of(context).textTheme.bodySmall!),
|
||||
).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -197,14 +204,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
) ??
|
||||
[]),
|
||||
DropdownMenuItem<SnRealm>(
|
||||
enabled: _editingChannel == null,
|
||||
value: null,
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.clear),
|
||||
),
|
||||
const Gap(12),
|
||||
@ -213,9 +220,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('fieldChatBelongToRealmUnset')
|
||||
.tr()
|
||||
.textStyle(
|
||||
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
],
|
||||
@ -231,10 +236,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 60,
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -250,8 +255,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
helperText: 'fieldChatAliasHint'.tr(),
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -260,8 +264,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -272,8 +275,24 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatDescription'.tr(),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
CheckboxListTile(
|
||||
value: _isPublic,
|
||||
title: Text('channelIsPublic'.tr()),
|
||||
subtitle: Text('channelIsPublicDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isPublic = value ?? false);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: _isCommunity,
|
||||
title: Text('channelIsCommunity'.tr()),
|
||||
subtitle: Text('channelIsCommunityDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isCommunity = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_detail.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -97,8 +94,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
@ -187,6 +182,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
@ -245,17 +261,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
return Center(
|
||||
child: OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
),
|
||||
return OpenablePostItem(
|
||||
data: _posts[idx],
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
_refreshPosts();
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
|
@ -131,6 +131,7 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Icon(Symbols.update),
|
||||
title: Text('updateAvailable').tr(),
|
||||
@ -180,6 +181,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
return Column(
|
||||
children: days.map((ele) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||
@ -203,6 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
|
||||
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||
final diff = nextOne.$2.difference(DateTime.now());
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
|
||||
@ -270,6 +273,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -469,6 +473,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -594,6 +599,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -667,11 +673,13 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: CircularProgressIndicator().center(),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -17,7 +16,6 @@ import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||
|
||||
class PostDetailScreen extends StatefulWidget {
|
||||
final String slug;
|
||||
@ -64,7 +62,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
@ -114,7 +113,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: 640,
|
||||
maxWidth: maxWidth,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
@ -125,11 +124,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -142,51 +141,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized)
|
||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 240,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
margin:
|
||||
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
: BorderRadius.zero,
|
||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
)
|
||||
: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: _data!.id,
|
||||
onPost: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
).center(),
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
onPosted: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null)
|
||||
if (_data != null && _data!.type != 'video')
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPost: _data!,
|
||||
maxWidth: 640,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -6,7 +6,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
@ -14,10 +16,15 @@ import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_input.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
|
||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -25,6 +32,8 @@ import 'package:surface/widgets/post/post_media_pending_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/widgets/post/post_poll_editor.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PostEditorExtra {
|
||||
final String? text;
|
||||
@ -102,7 +111,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@ -128,14 +137,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
builder: (context) => _PostPublisherPopup(
|
||||
controller: _writeController,
|
||||
publishers: _publishers,
|
||||
onUpdate: () {
|
||||
_fetchPublishers();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPollEditorDialog() async {
|
||||
final poll = await showDialog<dynamic>(
|
||||
context: context,
|
||||
builder: (context) => PollEditorDialog(
|
||||
poll: _writeController.poll,
|
||||
),
|
||||
);
|
||||
if (poll == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
if (poll == false) {
|
||||
_writeController.setPoll(null);
|
||||
} else {
|
||||
_writeController.setPoll(poll);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_writeController.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -225,12 +256,68 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_writeController.replyingPost != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.reply, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.replyingPost!.publisher.name}').bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_writeController.replyingPost!.body['content'],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_writeController.repostingPost != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.forward, size: 16),
|
||||
const Gap(10),
|
||||
Text('@${_writeController.repostingPost!.publisher.name}').bold(),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_writeController.repostingPost!.body['content'],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 160),
|
||||
child: switch (_writeController.mode) {
|
||||
child: StyledWidget(switch (_writeController.mode) {
|
||||
'stories' => _PostStoryEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
@ -243,8 +330,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
),
|
||||
'videos' => _PostVideoEditor(
|
||||
controller: _writeController,
|
||||
onTapPublisher: _showPublisherPopup,
|
||||
),
|
||||
_ => const Placeholder(),
|
||||
},
|
||||
})
|
||||
.padding(top: 8),
|
||||
),
|
||||
if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
|
||||
Positioned(
|
||||
@ -292,7 +384,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
if (_writeController.isBusy && _writeController.progress != null)
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: _writeController.progress),
|
||||
@ -301,6 +392,8 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
)
|
||||
else if (_writeController.isBusy)
|
||||
const LinearProgressIndicator(value: null, minHeight: 2),
|
||||
LoadingIndicator(isActive: _isLoading),
|
||||
const Gap(4),
|
||||
Container(
|
||||
child: _writeController.temporaryRestored
|
||||
? Container(
|
||||
@ -348,6 +441,18 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_writeController.mode == 'stories')
|
||||
IconButton(
|
||||
icon: Icon(Symbols.poll, color: Theme.of(context).colorScheme.primary),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: _writeController.poll == null
|
||||
? null
|
||||
: WidgetStatePropertyAll(Theme.of(context).colorScheme.surfaceContainer),
|
||||
),
|
||||
onPressed: () {
|
||||
_showPollEditorDialog();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -370,7 +475,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
],
|
||||
).padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
top: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -392,8 +496,9 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
|
||||
class _PostPublisherPopup extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
final List<SnPublisher>? publishers;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostPublisherPopup({required this.controller, this.publishers});
|
||||
const _PostPublisherPopup({required this.controller, this.publishers, required this.onUpdate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -408,6 +513,20 @@ class _PostPublisherPopup extends StatelessWidget {
|
||||
Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('publishersNew').tr(),
|
||||
subtitle: Text('publisherNewSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
if (value == true) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: publishers?.length ?? 0,
|
||||
@ -687,3 +806,226 @@ class _PostQuestionEditor extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
final Function? onTapPublisher;
|
||||
|
||||
const _PostVideoEditor({required this.controller, this.onTapPublisher});
|
||||
|
||||
void _selectVideo(BuildContext context) async {
|
||||
final video = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'postVideoUpload'.tr(),
|
||||
pool: 'interactive',
|
||||
mediaType: SnMediaType.video,
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (video == null) return;
|
||||
controller.setVideoAttachment(video);
|
||||
}
|
||||
|
||||
void _setAlt(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
controller.setVideoAttachment(result);
|
||||
}
|
||||
|
||||
Future<void> _createBoost(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final result = await showDialog<SnAttachmentBoost?>(
|
||||
context: context,
|
||||
builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final newAttach = controller.videoAttachment!.copyWith(
|
||||
boosts: [...controller.videoAttachment!.boosts, result],
|
||||
);
|
||||
|
||||
controller.setVideoAttachment(newAttach);
|
||||
}
|
||||
|
||||
void _setThumbnail(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
final thumbnail = await showDialog<SnAttachment?>(
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
if (thumbnail == null) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
try {
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
final newAttach = await attach.updateOne(
|
||||
controller.videoAttachment!,
|
||||
thumbnailId: thumbnail.id,
|
||||
);
|
||||
controller.setVideoAttachment(newAttach);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteAttachment(BuildContext context) async {
|
||||
if (controller.videoAttachment == null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
|
||||
controller.setVideoAttachment(null);
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
children: [
|
||||
AccountImage(content: controller.publisher?.avatar, radius: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
|
||||
Text('@${controller.publisher?.name}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
onTap: () {
|
||||
onTapPublisher?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: controller.titleController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: controller.descriptionController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'fieldPostDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 16),
|
||||
const Gap(12),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: [
|
||||
MenuItem(
|
||||
label: 'attachmentSetAlt'.tr(),
|
||||
icon: Symbols.description,
|
||||
onSelected: () {
|
||||
_setAlt(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentBoost'.tr(),
|
||||
icon: Symbols.bolt,
|
||||
onSelected: () {
|
||||
_createBoost(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentSetThumbnail'.tr(),
|
||||
icon: Symbols.image,
|
||||
onSelected: () {
|
||||
_setThumbnail(context);
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'attachmentCopyRandomId'.tr(),
|
||||
icon: Symbols.content_copy,
|
||||
onSelected: () {
|
||||
Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
|
||||
},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'delete'.tr(),
|
||||
icon: Symbols.delete,
|
||||
onSelected: () => _deleteAttachment(context),
|
||||
),
|
||||
MenuItem(
|
||||
label: 'unlink'.tr(),
|
||||
icon: Symbols.link_off,
|
||||
onSelected: () {
|
||||
controller.setVideoAttachment(null);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: controller.videoAttachment == null ? () => _selectVideo(context) : null,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: controller.videoAttachment == null
|
||||
? Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const Gap(4),
|
||||
Text('postVideoUpload'.tr()),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AttachmentItem(
|
||||
data: controller.videoAttachment!,
|
||||
heroTag: const Uuid().v4(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
@ -100,6 +100,12 @@ class _RealmScreenState extends State<RealmScreen> {
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenRealm').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.globe),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('realmDiscovery');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: !_isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
||||
onPressed: () {
|
||||
|
@ -50,6 +50,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
_aliasController.text = out.alias;
|
||||
_nameController.text = out.name;
|
||||
_descriptionController.text = out.description;
|
||||
_isPublic = out.isPublic;
|
||||
_isCommunity = out.isCommunity;
|
||||
} catch (err) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (context.mounted) context.showErrorDialog(err);
|
||||
@ -67,6 +69,9 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
Future<void> _updateImage(String place) async {
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
@ -138,6 +143,8 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
'description': _descriptionController.text,
|
||||
'avatar': _avatar,
|
||||
'banner': _banner,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -293,6 +300,23 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
CheckboxListTile(
|
||||
value: _isPublic,
|
||||
title: Text('realmIsPublic'.tr()),
|
||||
subtitle: Text('realmIsPublicDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isPublic = value ?? false);
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: _isCommunity,
|
||||
title: Text('realmIsCommunity'.tr()),
|
||||
subtitle: Text('realmIsCommunityDescription'.tr()),
|
||||
onChanged: (value) {
|
||||
setState(() => _isCommunity = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
@ -357,6 +357,28 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _leaveRealm() async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'realmLeave'.tr(),
|
||||
'realmLeaveDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
await sn.client.delete('/cgi/id/realms/${widget.realm!.alias}/members/me');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
@ -367,22 +389,31 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
|
||||
children: [
|
||||
const Gap(8),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
title: Text('realmLeave').tr(),
|
||||
subtitle: Text('realmLeaveDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
onTap: _isBusy ? null : () => _leaveRealm(),
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('realmEdit').tr(),
|
||||
subtitle: Text('realmEditDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmManage',
|
||||
queryParameters: {'editing': widget.realm!.alias},
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isOwned)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
|
290
lib/screens/realm/realm_discovery.dart
Normal file
290
lib/screens/realm/realm_discovery.dart
Normal file
@ -0,0 +1,290 @@
|
||||
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/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/universal_image.dart';
|
||||
|
||||
class RealmDiscoveryScreen extends StatefulWidget {
|
||||
const RealmDiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RealmDiscoveryScreen> createState() => _RealmDiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
||||
List<SnRealm>? _realms;
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchRealms() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms');
|
||||
_realms = List<SnRealm>.from(
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRealms();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('screenRealmDiscovery').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchRealms,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _realms?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final realm = _realms![idx];
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(12),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: (realm.banner?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(realm.banner!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
child: AccountImage(
|
||||
content: realm.avatar,
|
||||
radius: 24,
|
||||
fallbackWidget: const Icon(Symbols.group, size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(20 + 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
|
||||
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
|
||||
],
|
||||
).padding(horizontal: 24, bottom: 14),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _RealmJoinPopup(realm: realm),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmJoinPopup extends StatefulWidget {
|
||||
final SnRealm realm;
|
||||
|
||||
const _RealmJoinPopup({required this.realm});
|
||||
|
||||
@override
|
||||
State<_RealmJoinPopup> createState() => _RealmJoinPopupState();
|
||||
}
|
||||
|
||||
class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
final List<String> _planJoinChannels = List.empty(growable: true);
|
||||
|
||||
List<SnChannel>? _channels;
|
||||
bool _isBusy = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
Future<void> _fetchPublicChannels() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||
final out = List<SnChannel>.from(
|
||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||
);
|
||||
setState(() => _channels = out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinRealm() async {
|
||||
try {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
await _joinSelectedChannels();
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||
Navigator.pop(context);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isJoining = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinSelectedChannels() async {
|
||||
if (_planJoinChannels.isEmpty) return;
|
||||
for (final channel in _planJoinChannels) {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchPublicChannels();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.group_add, size: 24),
|
||||
const Gap(16),
|
||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.realm.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text(
|
||||
widget.realm.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isJoining ? null : () => _joinRealm(),
|
||||
child: Text('join'.tr()),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, bottom: 12),
|
||||
const Divider(height: 1),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final channel = _channels![index];
|
||||
return CheckboxListTile(
|
||||
value: _planJoinChannels.contains(channel.alias),
|
||||
title: Text(channel.name),
|
||||
subtitle: Text(
|
||||
channel.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
secondary: AccountImage(
|
||||
content: null,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
),
|
||||
onChanged: (value) {
|
||||
value ??= false;
|
||||
if (value) {
|
||||
setState(() => _planJoinChannels.add(channel.alias));
|
||||
} else {
|
||||
setState(() => _planJoinChannels.remove(channel.alias));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
45
lib/types/poll.dart
Normal file
45
lib/types/poll.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'poll.freezed.dart';
|
||||
part 'poll.g.dart';
|
||||
|
||||
@freezed
|
||||
class SnPoll with _$SnPoll {
|
||||
const factory SnPoll({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required dynamic expiredAt,
|
||||
required List<SnPollOption> options,
|
||||
required int accountId,
|
||||
required SnPollMetric metric,
|
||||
}) = _SnPoll;
|
||||
|
||||
factory SnPoll.fromJson(Map<String, Object?> json) => _$SnPollFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollMetric with _$SnPollMetric {
|
||||
const factory SnPollMetric({
|
||||
required int totalAnswer,
|
||||
@Default({}) Map<String, int> byOptions,
|
||||
@Default({}) Map<String, double> byOptionsPercentage,
|
||||
}) = _SnPollMetric;
|
||||
|
||||
factory SnPollMetric.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollMetricFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPollOption with _$SnPollOption {
|
||||
const factory SnPollOption({
|
||||
required String id,
|
||||
required String icon,
|
||||
required String name,
|
||||
required String description,
|
||||
}) = _SnPollOption;
|
||||
|
||||
factory SnPollOption.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPollOptionFromJson(json);
|
||||
}
|
761
lib/types/poll.freezed.dart
Normal file
761
lib/types/poll.freezed.dart
Normal file
@ -0,0 +1,761 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'poll.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
SnPoll _$SnPollFromJson(Map<String, dynamic> json) {
|
||||
return _SnPoll.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPoll {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
dynamic get expiredAt => throw _privateConstructorUsedError;
|
||||
List<SnPollOption> get options => throw _privateConstructorUsedError;
|
||||
int get accountId => throw _privateConstructorUsedError;
|
||||
SnPollMetric get metric => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPoll to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollCopyWith<SnPoll> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollCopyWith<$Res> {
|
||||
factory $SnPollCopyWith(SnPoll value, $Res Function(SnPoll) then) =
|
||||
_$SnPollCopyWithImpl<$Res, SnPoll>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollCopyWithImpl<$Res, $Val extends SnPoll>
|
||||
implements $SnPollCopyWith<$Res> {
|
||||
_$SnPollCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// 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? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value.options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollMetricCopyWith<$Res> get metric {
|
||||
return $SnPollMetricCopyWith<$Res>(_value.metric, (value) {
|
||||
return _then(_value.copyWith(metric: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollImplCopyWith<$Res> implements $SnPollCopyWith<$Res> {
|
||||
factory _$$SnPollImplCopyWith(
|
||||
_$SnPollImpl value, $Res Function(_$SnPollImpl) then) =
|
||||
__$$SnPollImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
dynamic expiredAt,
|
||||
List<SnPollOption> options,
|
||||
int accountId,
|
||||
SnPollMetric metric});
|
||||
|
||||
@override
|
||||
$SnPollMetricCopyWith<$Res> get metric;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollImplCopyWithImpl<$Res>
|
||||
extends _$SnPollCopyWithImpl<$Res, _$SnPollImpl>
|
||||
implements _$$SnPollImplCopyWith<$Res> {
|
||||
__$$SnPollImplCopyWithImpl(
|
||||
_$SnPollImpl _value, $Res Function(_$SnPollImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// 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? expiredAt = freezed,
|
||||
Object? options = null,
|
||||
Object? accountId = null,
|
||||
Object? metric = null,
|
||||
}) {
|
||||
return _then(_$SnPollImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
expiredAt: freezed == expiredAt
|
||||
? _value.expiredAt
|
||||
: expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
options: null == options
|
||||
? _value._options
|
||||
: options // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPollOption>,
|
||||
accountId: null == accountId
|
||||
? _value.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
metric: null == metric
|
||||
? _value.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnPollMetric,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollImpl implements _SnPoll {
|
||||
const _$SnPollImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required this.accountId,
|
||||
required this.metric})
|
||||
: _options = options;
|
||||
|
||||
factory _$SnPollImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final dynamic expiredAt;
|
||||
final List<SnPollOption> _options;
|
||||
@override
|
||||
List<SnPollOption> get options {
|
||||
if (_options is EqualUnmodifiableListView) return _options;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_options);
|
||||
}
|
||||
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final SnPollMetric metric;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPoll(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, expiredAt: $expiredAt, options: $options, accountId: $accountId, metric: $metric)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
const DeepCollectionEquality().equals(other.expiredAt, expiredAt) &&
|
||||
const DeepCollectionEquality().equals(other._options, _options) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.metric, metric) || other.metric == metric));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
const DeepCollectionEquality().hash(expiredAt),
|
||||
const DeepCollectionEquality().hash(_options),
|
||||
accountId,
|
||||
metric);
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
__$$SnPollImplCopyWithImpl<_$SnPollImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPoll implements SnPoll {
|
||||
const factory _SnPoll(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final dynamic expiredAt,
|
||||
required final List<SnPollOption> options,
|
||||
required final int accountId,
|
||||
required final SnPollMetric metric}) = _$SnPollImpl;
|
||||
|
||||
factory _SnPoll.fromJson(Map<String, dynamic> json) = _$SnPollImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
dynamic get expiredAt;
|
||||
@override
|
||||
List<SnPollOption> get options;
|
||||
@override
|
||||
int get accountId;
|
||||
@override
|
||||
SnPollMetric get metric;
|
||||
|
||||
/// Create a copy of SnPoll
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollImplCopyWith<_$SnPollImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollMetric _$SnPollMetricFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollMetric.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollMetric {
|
||||
int get totalAnswer => throw _privateConstructorUsedError;
|
||||
Map<String, int> get byOptions => throw _privateConstructorUsedError;
|
||||
Map<String, double> get byOptionsPercentage =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollMetric to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollMetricCopyWith<SnPollMetric> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollMetricCopyWith<$Res> {
|
||||
factory $SnPollMetricCopyWith(
|
||||
SnPollMetric value, $Res Function(SnPollMetric) then) =
|
||||
_$SnPollMetricCopyWithImpl<$Res, SnPollMetric>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollMetricCopyWithImpl<$Res, $Val extends SnPollMetric>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
_$SnPollMetricCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value.byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value.byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollMetricImplCopyWith<$Res>
|
||||
implements $SnPollMetricCopyWith<$Res> {
|
||||
factory _$$SnPollMetricImplCopyWith(
|
||||
_$SnPollMetricImpl value, $Res Function(_$SnPollMetricImpl) then) =
|
||||
__$$SnPollMetricImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int totalAnswer,
|
||||
Map<String, int> byOptions,
|
||||
Map<String, double> byOptionsPercentage});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollMetricImplCopyWithImpl<$Res>
|
||||
extends _$SnPollMetricCopyWithImpl<$Res, _$SnPollMetricImpl>
|
||||
implements _$$SnPollMetricImplCopyWith<$Res> {
|
||||
__$$SnPollMetricImplCopyWithImpl(
|
||||
_$SnPollMetricImpl _value, $Res Function(_$SnPollMetricImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? totalAnswer = null,
|
||||
Object? byOptions = null,
|
||||
Object? byOptionsPercentage = null,
|
||||
}) {
|
||||
return _then(_$SnPollMetricImpl(
|
||||
totalAnswer: null == totalAnswer
|
||||
? _value.totalAnswer
|
||||
: totalAnswer // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
byOptions: null == byOptions
|
||||
? _value._byOptions
|
||||
: byOptions // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>,
|
||||
byOptionsPercentage: null == byOptionsPercentage
|
||||
? _value._byOptionsPercentage
|
||||
: byOptionsPercentage // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, double>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollMetricImpl implements _SnPollMetric {
|
||||
const _$SnPollMetricImpl(
|
||||
{required this.totalAnswer,
|
||||
final Map<String, int> byOptions = const {},
|
||||
final Map<String, double> byOptionsPercentage = const {}})
|
||||
: _byOptions = byOptions,
|
||||
_byOptionsPercentage = byOptionsPercentage;
|
||||
|
||||
factory _$SnPollMetricImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollMetricImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int totalAnswer;
|
||||
final Map<String, int> _byOptions;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, int> get byOptions {
|
||||
if (_byOptions is EqualUnmodifiableMapView) return _byOptions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptions);
|
||||
}
|
||||
|
||||
final Map<String, double> _byOptionsPercentage;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, double> get byOptionsPercentage {
|
||||
if (_byOptionsPercentage is EqualUnmodifiableMapView)
|
||||
return _byOptionsPercentage;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_byOptionsPercentage);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollMetric(totalAnswer: $totalAnswer, byOptions: $byOptions, byOptionsPercentage: $byOptionsPercentage)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollMetricImpl &&
|
||||
(identical(other.totalAnswer, totalAnswer) ||
|
||||
other.totalAnswer == totalAnswer) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptions, _byOptions) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._byOptionsPercentage, _byOptionsPercentage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
totalAnswer,
|
||||
const DeepCollectionEquality().hash(_byOptions),
|
||||
const DeepCollectionEquality().hash(_byOptionsPercentage));
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
__$$SnPollMetricImplCopyWithImpl<_$SnPollMetricImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollMetricImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollMetric implements SnPollMetric {
|
||||
const factory _SnPollMetric(
|
||||
{required final int totalAnswer,
|
||||
final Map<String, int> byOptions,
|
||||
final Map<String, double> byOptionsPercentage}) = _$SnPollMetricImpl;
|
||||
|
||||
factory _SnPollMetric.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollMetricImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get totalAnswer;
|
||||
@override
|
||||
Map<String, int> get byOptions;
|
||||
@override
|
||||
Map<String, double> get byOptionsPercentage;
|
||||
|
||||
/// Create a copy of SnPollMetric
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollMetricImplCopyWith<_$SnPollMetricImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPollOption _$SnPollOptionFromJson(Map<String, dynamic> json) {
|
||||
return _SnPollOption.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPollOption {
|
||||
String get id => throw _privateConstructorUsedError;
|
||||
String get icon => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPollOption to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPollOptionCopyWith<SnPollOption> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPollOptionCopyWith<$Res> {
|
||||
factory $SnPollOptionCopyWith(
|
||||
SnPollOption value, $Res Function(SnPollOption) then) =
|
||||
_$SnPollOptionCopyWithImpl<$Res, SnPollOption>;
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPollOptionCopyWithImpl<$Res, $Val extends SnPollOption>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
_$SnPollOptionCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPollOptionImplCopyWith<$Res>
|
||||
implements $SnPollOptionCopyWith<$Res> {
|
||||
factory _$$SnPollOptionImplCopyWith(
|
||||
_$SnPollOptionImpl value, $Res Function(_$SnPollOptionImpl) then) =
|
||||
__$$SnPollOptionImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String id, String icon, String name, String description});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPollOptionImplCopyWithImpl<$Res>
|
||||
extends _$SnPollOptionCopyWithImpl<$Res, _$SnPollOptionImpl>
|
||||
implements _$$SnPollOptionImplCopyWith<$Res> {
|
||||
__$$SnPollOptionImplCopyWithImpl(
|
||||
_$SnPollOptionImpl _value, $Res Function(_$SnPollOptionImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? icon = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
}) {
|
||||
return _then(_$SnPollOptionImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
icon: null == icon
|
||||
? _value.icon
|
||||
: icon // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPollOptionImpl implements _SnPollOption {
|
||||
const _$SnPollOptionImpl(
|
||||
{required this.id,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.description});
|
||||
|
||||
factory _$SnPollOptionImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPollOptionImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String id;
|
||||
@override
|
||||
final String icon;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String description;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPollOption(id: $id, icon: $icon, name: $name, description: $description)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPollOptionImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.icon, icon) || other.icon == icon) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, icon, name, description);
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
__$$SnPollOptionImplCopyWithImpl<_$SnPollOptionImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPollOptionImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPollOption implements SnPollOption {
|
||||
const factory _SnPollOption(
|
||||
{required final String id,
|
||||
required final String icon,
|
||||
required final String name,
|
||||
required final String description}) = _$SnPollOptionImpl;
|
||||
|
||||
factory _SnPollOption.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPollOptionImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get id;
|
||||
@override
|
||||
String get icon;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get description;
|
||||
|
||||
/// Create a copy of SnPollOption
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPollOptionImplCopyWith<_$SnPollOptionImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
69
lib/types/poll.g.dart
Normal file
69
lib/types/poll.g.dart
Normal file
@ -0,0 +1,69 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'poll.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SnPollImpl _$$SnPollImplFromJson(Map<String, dynamic> json) => _$SnPollImpl(
|
||||
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'],
|
||||
expiredAt: json['expired_at'],
|
||||
options: (json['options'] as List<dynamic>)
|
||||
.map((e) => SnPollOption.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
metric: SnPollMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollImplToJson(_$SnPollImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'expired_at': instance.expiredAt,
|
||||
'options': instance.options.map((e) => e.toJson()).toList(),
|
||||
'account_id': instance.accountId,
|
||||
'metric': instance.metric.toJson(),
|
||||
};
|
||||
|
||||
_$SnPollMetricImpl _$$SnPollMetricImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollMetricImpl(
|
||||
totalAnswer: (json['total_answer'] as num).toInt(),
|
||||
byOptions: (json['by_options'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
) ??
|
||||
const {},
|
||||
byOptionsPercentage:
|
||||
(json['by_options_percentage'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||
) ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollMetricImplToJson(_$SnPollMetricImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'total_answer': instance.totalAnswer,
|
||||
'by_options': instance.byOptions,
|
||||
'by_options_percentage': instance.byOptionsPercentage,
|
||||
};
|
||||
|
||||
_$SnPollOptionImpl _$$SnPollOptionImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPollOptionImpl(
|
||||
id: json['id'] as String,
|
||||
icon: json['icon'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPollOptionImplToJson(_$SnPollOptionImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'icon': instance.icon,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
|
||||
part 'post.freezed.dart';
|
||||
part 'post.g.dart';
|
||||
@ -37,6 +38,7 @@ class SnPost with _$SnPost {
|
||||
required int totalUpvote,
|
||||
required int totalDownvote,
|
||||
required int publisherId,
|
||||
required int? pollId,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
SnPostPreload? preload,
|
||||
@ -89,6 +91,8 @@ class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
required SnAttachment? thumbnail,
|
||||
required List<SnAttachment?>? attachments,
|
||||
required SnAttachment? video,
|
||||
required SnPoll? poll,
|
||||
}) = _SnPostPreload;
|
||||
|
||||
factory SnPostPreload.fromJson(Map<String, Object?> json) =>
|
||||
|
@ -48,6 +48,7 @@ mixin _$SnPost {
|
||||
int get totalUpvote => throw _privateConstructorUsedError;
|
||||
int get totalDownvote => throw _privateConstructorUsedError;
|
||||
int get publisherId => throw _privateConstructorUsedError;
|
||||
int? get pollId => throw _privateConstructorUsedError;
|
||||
SnPublisher get publisher => throw _privateConstructorUsedError;
|
||||
SnMetric get metric => throw _privateConstructorUsedError;
|
||||
SnPostPreload? get preload => throw _privateConstructorUsedError;
|
||||
@ -95,6 +96,7 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -149,6 +151,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -266,6 +269,10 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -380,6 +387,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
@ -437,6 +445,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
Object? totalUpvote = null,
|
||||
Object? totalDownvote = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
@ -554,6 +563,10 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
? _value.publisherId
|
||||
: publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
pollId: freezed == pollId
|
||||
? _value.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
publisher: null == publisher
|
||||
? _value.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -602,6 +615,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.totalUpvote,
|
||||
required this.totalDownvote,
|
||||
required this.publisherId,
|
||||
required this.pollId,
|
||||
required this.publisher,
|
||||
required this.metric,
|
||||
this.preload})
|
||||
@ -719,6 +733,8 @@ class _$SnPostImpl extends _SnPost {
|
||||
@override
|
||||
final int publisherId;
|
||||
@override
|
||||
final int? pollId;
|
||||
@override
|
||||
final SnPublisher publisher;
|
||||
@override
|
||||
final SnMetric metric;
|
||||
@ -727,7 +743,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -782,6 +798,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
other.publisher == publisher) &&
|
||||
(identical(other.metric, metric) || other.metric == metric) &&
|
||||
@ -820,6 +837,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
publisherId,
|
||||
pollId,
|
||||
publisher,
|
||||
metric,
|
||||
preload
|
||||
@ -871,6 +889,7 @@ abstract class _SnPost extends SnPost {
|
||||
required final int totalUpvote,
|
||||
required final int totalDownvote,
|
||||
required final int publisherId,
|
||||
required final int? pollId,
|
||||
required final SnPublisher publisher,
|
||||
required final SnMetric metric,
|
||||
final SnPostPreload? preload}) = _$SnPostImpl;
|
||||
@ -935,6 +954,8 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
int get publisherId;
|
||||
@override
|
||||
int? get pollId;
|
||||
@override
|
||||
SnPublisher get publisher;
|
||||
@override
|
||||
SnMetric get metric;
|
||||
@ -1567,6 +1588,8 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||
mixin _$SnPostPreload {
|
||||
SnAttachment? get thumbnail => throw _privateConstructorUsedError;
|
||||
List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
|
||||
SnAttachment? get video => throw _privateConstructorUsedError;
|
||||
SnPoll? get poll => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPostPreload to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@ -1584,9 +1607,15 @@ abstract class $SnPostPreloadCopyWith<$Res> {
|
||||
SnPostPreload value, $Res Function(SnPostPreload) then) =
|
||||
_$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1606,6 +1635,8 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1616,6 +1647,14 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
? _value.attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@ -1632,6 +1671,34 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
|
||||
return _then(_value.copyWith(thumbnail: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAttachmentCopyWith<$Res>? get video {
|
||||
if (_value.video == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
|
||||
return _then(_value.copyWith(video: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollCopyWith<$Res>? get poll {
|
||||
if (_value.poll == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollCopyWith<$Res>(_value.poll!, (value) {
|
||||
return _then(_value.copyWith(poll: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1642,10 +1709,18 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
|
||||
__$$SnPostPreloadImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
|
||||
$Res call(
|
||||
{SnAttachment? thumbnail,
|
||||
List<SnAttachment?>? attachments,
|
||||
SnAttachment? video,
|
||||
SnPoll? poll});
|
||||
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get video;
|
||||
@override
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -1663,6 +1738,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
$Res call({
|
||||
Object? thumbnail = freezed,
|
||||
Object? attachments = freezed,
|
||||
Object? video = freezed,
|
||||
Object? poll = freezed,
|
||||
}) {
|
||||
return _then(_$SnPostPreloadImpl(
|
||||
thumbnail: freezed == thumbnail
|
||||
@ -1673,6 +1750,14 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
? _value._attachments
|
||||
: attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnAttachment?>?,
|
||||
video: freezed == video
|
||||
? _value.video
|
||||
: video // ignore: cast_nullable_to_non_nullable
|
||||
as SnAttachment?,
|
||||
poll: freezed == poll
|
||||
? _value.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1682,7 +1767,9 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
|
||||
class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
const _$SnPostPreloadImpl(
|
||||
{required this.thumbnail,
|
||||
required final List<SnAttachment?>? attachments})
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required this.video,
|
||||
required this.poll})
|
||||
: _attachments = attachments;
|
||||
|
||||
factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
|
||||
@ -1700,9 +1787,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final SnAttachment? video;
|
||||
@override
|
||||
final SnPoll? poll;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
|
||||
return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video, poll: $poll)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1713,13 +1805,15 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
(identical(other.thumbnail, thumbnail) ||
|
||||
other.thumbnail == thumbnail) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._attachments, _attachments));
|
||||
.equals(other._attachments, _attachments) &&
|
||||
(identical(other.video, video) || other.video == video) &&
|
||||
(identical(other.poll, poll) || other.poll == poll));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, thumbnail,
|
||||
const DeepCollectionEquality().hash(_attachments));
|
||||
const DeepCollectionEquality().hash(_attachments), video, poll);
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -1740,7 +1834,9 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
|
||||
abstract class _SnPostPreload implements SnPostPreload {
|
||||
const factory _SnPostPreload(
|
||||
{required final SnAttachment? thumbnail,
|
||||
required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
|
||||
required final List<SnAttachment?>? attachments,
|
||||
required final SnAttachment? video,
|
||||
required final SnPoll? poll}) = _$SnPostPreloadImpl;
|
||||
|
||||
factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPostPreloadImpl.fromJson;
|
||||
@ -1749,6 +1845,10 @@ abstract class _SnPostPreload implements SnPostPreload {
|
||||
SnAttachment? get thumbnail;
|
||||
@override
|
||||
List<SnAttachment?>? get attachments;
|
||||
@override
|
||||
SnAttachment? get video;
|
||||
@override
|
||||
SnPoll? get poll;
|
||||
|
||||
/// Create a copy of SnPostPreload
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
|
@ -63,6 +63,7 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
totalUpvote: (json['total_upvote'] as num).toInt(),
|
||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||
publisherId: (json['publisher_id'] as num).toInt(),
|
||||
pollId: (json['poll_id'] as num?)?.toInt(),
|
||||
publisher:
|
||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
@ -101,6 +102,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'total_upvote': instance.totalUpvote,
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'publisher_id': instance.publisherId,
|
||||
'poll_id': instance.pollId,
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
'preload': instance.preload?.toJson(),
|
||||
@ -165,12 +167,20 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
video: json['video'] == null
|
||||
? null
|
||||
: SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
|
||||
poll: json['poll'] == null
|
||||
? null
|
||||
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
|
||||
'video': instance.video?.toJson(),
|
||||
'poll': instance.poll?.toJson(),
|
||||
};
|
||||
|
||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
|
||||
|
@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
|
||||
@ -49,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
Future<void> _getFriends() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
||||
if (!mounted) return;
|
||||
final ua = context.read<UserProvider>();
|
||||
|
||||
setState(() {
|
||||
_relativeUsers.addAll(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
resp.data?.map((e) {
|
||||
final rel = SnRelationship.fromJson(e);
|
||||
if (rel.relatedId == ua.user?.id) {
|
||||
return rel.account!;
|
||||
} else {
|
||||
return rel.related!;
|
||||
}
|
||||
}).cast<SnAccount>(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -123,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _pendingUsers.isEmpty
|
||||
? _relativeUsers.length
|
||||
: _pendingUsers.length,
|
||||
itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
|
||||
itemBuilder: (context, index) {
|
||||
var user = _pendingUsers.isEmpty
|
||||
? _relativeUsers[index]
|
||||
: _pendingUsers[index];
|
||||
var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
|
||||
return ListTile(
|
||||
title: Text(user.nick),
|
||||
subtitle: Text(user.name),
|
||||
@ -148,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final idx = _selectedUsers
|
||||
.indexWhere((x) => x.id == user.id);
|
||||
final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
|
||||
if (idx != -1) {
|
||||
_selectedUsers.removeAt(idx);
|
||||
} else {
|
||||
|
@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class AttachmentInputDialog extends StatefulWidget {
|
||||
final String? title;
|
||||
final bool? analyzeNow;
|
||||
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||
final bool? analyzeNow;
|
||||
final SnMediaType? mediaType;
|
||||
final String pool;
|
||||
|
||||
const AttachmentInputDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.pool,
|
||||
this.analyzeNow = false,
|
||||
this.mediaType = SnMediaType.image,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||
@ -20,13 +30,18 @@ final bool? analyzeNow;
|
||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
final _randomIdController = TextEditingController();
|
||||
|
||||
XFile? _thumbnailFile;
|
||||
XFile? _file;
|
||||
double? _progress;
|
||||
|
||||
void _pickImage() async {
|
||||
void _pickMedia() async {
|
||||
final picker = ImagePicker();
|
||||
final result = await picker.pickImage(source: ImageSource.gallery);
|
||||
final result = switch (widget.mediaType) {
|
||||
SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
|
||||
SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
|
||||
_ => await picker.pickMedia(),
|
||||
};
|
||||
if (result == null) return;
|
||||
setState(() => _thumbnailFile = result);
|
||||
setState(() => _file = result);
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
} else if (_thumbnailFile != null) {
|
||||
} else if (_file != null) {
|
||||
try {
|
||||
final attachment = await attach.directUploadOne(
|
||||
(await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
|
||||
_thumbnailFile!.path,
|
||||
'interactive',
|
||||
null,
|
||||
final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
|
||||
|
||||
final attachment = await attach.chunkedUploadParts(
|
||||
_file!,
|
||||
place.$1,
|
||||
place.$2,
|
||||
analyzeNow: widget.analyzeNow ?? false,
|
||||
onProgress: (value) {
|
||||
setState(() => _progress = value);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, attachment);
|
||||
} catch (err) {
|
||||
@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.title ?? 'attachmentInputDialog').tr(),
|
||||
title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -86,24 +106,35 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||
const Gap(24),
|
||||
Text('attachmentInputNew').tr().fontSize(14),
|
||||
Card(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickImage();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: const Icon(Symbols.add_photo_alternate),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('addAttachmentFromAlbum').tr(),
|
||||
subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||
onTap: () {
|
||||
_pickMedia();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_isBusy)
|
||||
LinearProgressIndicator(
|
||||
value: _progress,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
).padding(top: 16),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text('dialogDismiss').tr(),
|
||||
),
|
||||
TextButton(
|
||||
|
@ -45,7 +45,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
|
||||
final HotKey _pasteHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.keyV,
|
||||
modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
final HotKey _newLineHotKey = HotKey(
|
||||
key: PhysicalKeyboardKey.enter,
|
||||
modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
);
|
||||
|
||||
@ -61,6 +66,10 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
));
|
||||
setState(() {});
|
||||
});
|
||||
hotKeyManager.register(_newLineHotKey, keyDownHandler: (_) async {
|
||||
if (_contentController.text.isEmpty) return;
|
||||
_contentController.text += '\n';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -112,6 +121,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_contentController.text.isEmpty && _attachments.isEmpty) return;
|
||||
if (_isBusy) return;
|
||||
|
||||
final attach = context.read<SnAttachmentProvider>();
|
||||
@ -203,7 +213,11 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
void dispose() {
|
||||
_contentController.dispose();
|
||||
_focusNode.dispose();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
|
||||
_dismissEmojiPicker();
|
||||
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
|
||||
hotKeyManager.unregister(_pasteHotKey);
|
||||
hotKeyManager.unregister(_newLineHotKey);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -343,6 +357,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
_sendMessage();
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -351,10 +366,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.mood,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
_showEmojiPicker(context);
|
||||
},
|
||||
@ -372,10 +386,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
||||
Symbols.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -58,6 +58,7 @@ class AppScaffold extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
body: SizedBox.expand(
|
||||
child: AppBackground(
|
||||
isRoot: true,
|
||||
child: Column(
|
||||
children: [
|
||||
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
|
||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
@ -15,6 +16,47 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../../providers/sn_network.dart';
|
||||
|
||||
class PostCommentQuickAction extends StatelessWidget {
|
||||
final double? maxWidth;
|
||||
final SnPost parentPost;
|
||||
final Function? onPosted;
|
||||
|
||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
return Container(
|
||||
height: 240,
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? const BorderRadius.all(Radius.circular(8))
|
||||
: BorderRadius.zero,
|
||||
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||
? Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
)
|
||||
: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1 / devicePixelRatio,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: PostMiniEditor(
|
||||
postReplyId: parentPost.id,
|
||||
onPost: () {
|
||||
onPosted?.call();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostCommentSliverList extends StatefulWidget {
|
||||
final SnPost parentPost;
|
||||
final double? maxWidth;
|
||||
@ -71,6 +113,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
||||
|
||||
Future<void> refresh() async {
|
||||
_posts.clear();
|
||||
_postCount = null;
|
||||
_fetchPosts();
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/reaction.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/link_preview.dart';
|
||||
@ -35,6 +36,7 @@ import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_meta_editor.dart';
|
||||
import 'package:surface/widgets/post/post_poll.dart';
|
||||
import 'package:surface/widgets/post/post_reaction.dart';
|
||||
import 'package:surface/widgets/post/publisher_popover.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -68,32 +70,34 @@ class OpenablePostItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
|
||||
return OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: PostItem(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
showComments: showComments,
|
||||
showFullPost: showFullPost,
|
||||
onChanged: onChanged,
|
||||
onDeleted: onDeleted,
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: data.id.toString(),
|
||||
preload: data,
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
return Center(
|
||||
child: OpenContainer(
|
||||
closedBuilder: (_, __) => Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: PostItem(
|
||||
data: data,
|
||||
maxWidth: maxWidth,
|
||||
showComments: showComments,
|
||||
showFullPost: showFullPost,
|
||||
onChanged: onChanged,
|
||||
onDeleted: onDeleted,
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
openBuilder: (_, close) => PostDetailScreen(
|
||||
slug: data.id.toString(),
|
||||
preload: data,
|
||||
onBack: close,
|
||||
),
|
||||
openColor: Colors.transparent,
|
||||
openElevation: 0,
|
||||
transitionType: ContainerTransitionType.fade,
|
||||
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
|
||||
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
|
||||
),
|
||||
closedShape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -192,6 +196,57 @@ class PostItem extends StatelessWidget {
|
||||
final ua = context.read<UserProvider>();
|
||||
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
|
||||
|
||||
// Video full view
|
||||
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostContentHeader(
|
||||
data: data,
|
||||
isAuthor: isAuthor,
|
||||
isRelativeDate: !showFullPost,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onSelectAnswer: onSelectAnswer,
|
||||
onDeleted: () {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
|
||||
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
|
||||
_PostFeaturedComment(data: data),
|
||||
_PostBottomAction(
|
||||
data: data,
|
||||
showComments: true,
|
||||
showReactions: showReactions,
|
||||
onShare: () => _doShare(context),
|
||||
onShareImage: () => _doShareViaPicture(context),
|
||||
onChanged: _onChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
SizedBox(
|
||||
width: 340,
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
PostCommentSliverList(
|
||||
parentPost: data,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Article headline preview
|
||||
if (!showFullPost && data.type == 'article') {
|
||||
return Container(
|
||||
@ -210,6 +265,7 @@ class PostItem extends StatelessWidget {
|
||||
if (onDeleted != null) {}
|
||||
},
|
||||
).padding(horizontal: 12, top: 8, bottom: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
|
||||
@ -293,6 +349,7 @@ class PostItem extends StatelessWidget {
|
||||
if (onDeleted != null) onDeleted!();
|
||||
},
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
|
||||
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
|
||||
if (data.body['title'] != null || data.body['description'] != null)
|
||||
_PostHeadline(
|
||||
@ -333,6 +390,7 @@ class PostItem extends StatelessWidget {
|
||||
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
|
||||
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
|
||||
LinkPreviewWidget(
|
||||
text: data.body['content'],
|
||||
@ -907,7 +965,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'replying': data.id.toString()},
|
||||
);
|
||||
},
|
||||
@ -923,7 +981,7 @@ class _PostContentHeader extends StatelessWidget {
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
pathParameters: {'mode': data.typePlural},
|
||||
pathParameters: {'mode': 'stories'},
|
||||
queryParameters: {'reposting': data.id.toString()},
|
||||
);
|
||||
},
|
||||
@ -1520,3 +1578,29 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostVideoPlayer extends StatelessWidget {
|
||||
final SnPost data;
|
||||
|
||||
const _PostVideoPlayer({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AttachmentInputDialog(
|
||||
title: 'attachmentSetThumbnail'.tr(),
|
||||
pool: 'interactive',
|
||||
analyzeNow: true,
|
||||
),
|
||||
);
|
||||
|
138
lib/widgets/post/post_poll.dart
Normal file
138
lib/widgets/post/post_poll.dart
Normal file
@ -0,0 +1,138 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/providers/userinfo.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
|
||||
class PostPoll extends StatefulWidget {
|
||||
final SnPoll poll;
|
||||
|
||||
const PostPoll({super.key, required this.poll});
|
||||
|
||||
@override
|
||||
State<PostPoll> createState() => _PostPollState();
|
||||
}
|
||||
|
||||
class _PostPollState extends State<PostPoll> {
|
||||
bool _isBusy = false;
|
||||
late SnPoll _poll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_poll = widget.poll;
|
||||
_fetchAnswer();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String? _answeredChoice;
|
||||
|
||||
Future<void> _refreshPoll() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/polls/${widget.poll.id}');
|
||||
if (!mounted) return;
|
||||
setState(() => _poll = SnPoll.fromJson(resp.data!));
|
||||
}
|
||||
|
||||
Future<void> _fetchAnswer() async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp =
|
||||
await sn.client.get('/cgi/co/polls/${widget.poll.id}/answer');
|
||||
_answeredChoice = resp.data?['answer'];
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
// ignore because it may not found
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _voteForOption(SnPollOption option) async {
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/co/polls/${widget.poll.id}/answer', data: {
|
||||
'answer': option.id,
|
||||
});
|
||||
if (!mounted) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
_answeredChoice = option.id;
|
||||
_refreshPoll();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (final option in _poll.options)
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: MediaQuery.of(context).size.width *
|
||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||
.toDouble(),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
minTileHeight: 60,
|
||||
leading: _answeredChoice == option.id
|
||||
? const Icon(Symbols.circle, fill: 1)
|
||||
: const Icon(Symbols.circle),
|
||||
title: Text(option.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'pollVotes'
|
||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
||||
),
|
||||
Text(' · ').padding(horizontal: 4),
|
||||
Text(
|
||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (option.description.isNotEmpty)
|
||||
Text(option.description),
|
||||
],
|
||||
),
|
||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
201
lib/widgets/post/post_poll_editor.dart
Normal file
201
lib/widgets/post/post_poll_editor.dart
Normal file
@ -0,0 +1,201 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class PollEditorDialog extends StatefulWidget {
|
||||
final SnPoll? poll;
|
||||
|
||||
const PollEditorDialog({super.key, this.poll});
|
||||
|
||||
@override
|
||||
State<PollEditorDialog> createState() => _PollEditorDialogState();
|
||||
}
|
||||
|
||||
class _PollEditorDialogState extends State<PollEditorDialog> {
|
||||
final TextEditingController _linkController = TextEditingController();
|
||||
final List<SnPollOption> _pollOptions = List.empty(growable: true);
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _fetchPoll() async {
|
||||
if (_linkController.text.isEmpty) return;
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/co/polls/${_linkController.text}');
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyPost() async {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = widget.poll == null
|
||||
? await sn.client.post('/cgi/co/polls', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
})
|
||||
: await sn.client.put('/cgi/co/polls/${widget.poll!.id}', data: {
|
||||
'options': _pollOptions.where((ele) => ele.name.isNotEmpty).toList(),
|
||||
});
|
||||
final out = SnPoll.fromJson(resp.data);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, out);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deletePoll() async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'pollEditorDelete'.tr(),
|
||||
'pollEditorDeleteDescription'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/co/polls/${widget.poll!.id}');
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, false);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pollOptions.addAll(widget.poll?.options ?? []);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_linkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.poll == null ? 'pollEditorNew' : 'pollEditorEdit').tr(),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
if (widget.poll == null)
|
||||
TextField(
|
||||
controller: _linkController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: 'pollLinkExisting'.tr(),
|
||||
prefixText: '#',
|
||||
suffixIcon: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _isBusy ? null : () => _fetchPoll(),
|
||||
icon: const Icon(Icons.keyboard_arrow_right),
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < _pollOptions.length; i++)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.circle),
|
||||
title: TextFormField(
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'pollOptionName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
initialValue: _pollOptions[i].name,
|
||||
onChanged: (value) {
|
||||
// Looks like we don't need set state here cuz it got internal updated.
|
||||
_pollOptions[i] = _pollOptions[i].copyWith(name: value);
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
setState(() => _pollOptions.removeAt(i));
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('pollOptionAdd').tr(),
|
||||
onTap: () {
|
||||
setState(
|
||||
() => _pollOptions.add(
|
||||
SnPollOption(id: const Uuid().v4(), icon: '', name: '', description: ''),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.poll != null)
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorDelete').tr(),
|
||||
onTap: _isBusy ? null : () => _deletePoll(),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.link_off),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('pollEditorUnlink').tr(),
|
||||
onTap: _isBusy ? null : () => Navigator.pop(context, false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _applyPost(),
|
||||
child: Text('dialogConfirm'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ PODS:
|
||||
- bitsdojo_window_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- croppy (0.0.1):
|
||||
- FlutterMacOS
|
||||
@ -143,7 +142,7 @@ PODS:
|
||||
- HotKey
|
||||
- in_app_review (2.0.0):
|
||||
- FlutterMacOS
|
||||
- livekit_client (2.3.5):
|
||||
- livekit_client (2.3.6):
|
||||
- flutter_webrtc
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
@ -190,7 +189,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||
@ -244,7 +243,7 @@ EXTERNAL SOURCES:
|
||||
bitsdojo_window_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
|
||||
connectivity_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||
croppy:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
|
||||
device_info_plus:
|
||||
@ -308,7 +307,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
|
||||
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
|
||||
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
|
||||
croppy: 25a638bd7d05411d8c697f481568f261037694fc
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
|
||||
@ -334,7 +333,7 @@ SPEC CHECKSUMS:
|
||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||
livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
|
||||
livekit_client: 0ad107154753a5a76802d2222c040223ad049499
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
|
86
pubspec.lock
86
pubspec.lock
@ -138,26 +138,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948"
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e"
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573"
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.14"
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -214,6 +214,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
chalkdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chalkdart
|
||||
sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -266,10 +274,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
|
||||
sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -346,10 +354,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_webrtc
|
||||
sha256: e65506edb452148220efab53d8d2f8bb9d827bd8bcd53cf3a3e6df70b27f3d86
|
||||
sha256: "3b3ff59c66cbc1577ed0f28d7005b5163555208fb1697a42207424ab8baa27c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.10"
|
||||
version: "1.5.0"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -362,10 +370,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
|
||||
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.2"
|
||||
version: "11.3.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -490,10 +498,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
version: "8.3.7"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -764,10 +772,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: b3ff1ef5fb3924ee02b4d38b974ffae3969d50603e68787684ee9dd45f6f144a
|
||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6+1"
|
||||
version: "0.7.6+2"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -830,10 +838,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
|
||||
sha256: "572df3de6c828e571db4b75b4a96a15c2f34fa3d420a84438f44a3158b22e81a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.7"
|
||||
version: "0.12.9"
|
||||
freezed:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -886,10 +894,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
|
||||
sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.7.2"
|
||||
version: "14.8.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -934,10 +942,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: home_widget
|
||||
sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
|
||||
sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.0+1"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1035,7 +1043,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
|
||||
@ -1190,10 +1198,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
|
||||
sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.3.6"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1246,10 +1254,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: material_symbols_icons
|
||||
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
|
||||
sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2801.1"
|
||||
version: "4.2805.1"
|
||||
media_kit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1382,10 +1390,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.0"
|
||||
version: "8.2.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1758,18 +1766,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.5.2"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
|
||||
sha256: ea86be7b7114f9e94fddfbb52649e59a03d6627ccd2387ebddcd6624719e9f16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.5"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1822,10 +1830,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2179,10 +2187,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_compress
|
||||
sha256: "5b42d89f3970c956bad7a86c29682b0892c11a4ddf95ae6e29897ee28788e377"
|
||||
sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2251,10 +2259,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webrtc_interface
|
||||
sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
|
||||
sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.3.2+64
|
||||
version: 2.3.2+69
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@ -120,6 +120,7 @@ dependencies:
|
||||
xml: ^6.5.0
|
||||
tray_manager: ^0.3.2
|
||||
hotkey_manager: ^0.2.3
|
||||
image_picker_android: ^0.8.12+20
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user