Compare commits
45 Commits
9bdd08d8dd
...
3.2.0+134
Author | SHA1 | Date | |
---|---|---|---|
28b6eade48
|
|||
1de7ef8c96 | |||
67eac5dcf5 | |||
7a44bfa075
|
|||
1c2f25a152
|
|||
be26ea280e
|
|||
b4996d069f
|
|||
bf4892b34d
|
|||
5f84751fd5
|
|||
457d1bac60
|
|||
02ec11845b
|
|||
612f1bf004
|
|||
fd80b713ad
|
|||
508805368c
|
|||
98eb28a4ec
|
|||
d1a2f59dd1
|
|||
bb9adb963a
|
|||
83e40cd860
|
|||
c06fb12f6a
|
|||
6600cf4df8
|
|||
4293daaa2f
|
|||
866674ddde
|
|||
27d478ba4f
|
|||
cccade763f
|
|||
f760b85186
|
|||
e68c5f4f92
|
|||
b0f3b6b5c3
|
|||
cb2af379fa
|
|||
38f8103265
|
|||
06bb18bdaa
|
|||
84c38500d0
|
|||
9529bbf08b
|
|||
8baf77bcf7
|
|||
b2ac5fbef2
|
|||
c79b1d7aab
|
|||
|
4f55a8209c | ||
|
ace302111a | ||
|
1391fa0dde | ||
|
cbdc7acdcd | ||
|
b80d91825a | ||
|
1a703b7eba | ||
|
3621ea7744 | ||
|
b638343f02 | ||
|
269a64cabb | ||
406e5187a8
|
@@ -30,6 +30,8 @@
|
||||
"fieldEmailAddressMustBeValid": "The email address must be valid.",
|
||||
"logout": "Logout",
|
||||
"updateYourProfile": "Profile Settings",
|
||||
"settingsDefaultPool": "Default file pool",
|
||||
"settingsDefaultPoolHelper": "Select the default storage pool for file uploads",
|
||||
"accountBasicInfo": "Basic Info",
|
||||
"accountProfile": "Your Profile",
|
||||
"saveChanges": "Save Changes",
|
||||
@@ -168,6 +170,7 @@
|
||||
"addPhoto": "Add photo",
|
||||
"addAudio": "Add audio",
|
||||
"addFile": "Add file",
|
||||
"uploadFile": "Upload File",
|
||||
"recordAudio": "Record Audio",
|
||||
"linkAttachment": "Link Attachment",
|
||||
"fileIdCannotBeEmpty": "File ID cannot be empty",
|
||||
@@ -261,14 +264,14 @@
|
||||
"createStickerPack": "Create a Sticker Pack",
|
||||
"editStickerPack": "Edit Sticker Pack",
|
||||
"deleteStickerPack": "Delete Sticker Pack",
|
||||
"deleteStickerPackHint": "Are you sure to delete this sticker pack? This action cannot be undone.",
|
||||
"deleteStickerPackHint": "Are you sure you want to delete this sticker pack? This action cannot be undone.",
|
||||
"stickerPackPrefix": "Prefix",
|
||||
"stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.",
|
||||
"stickers": "Stickers",
|
||||
"createSticker": "Create a Sticker",
|
||||
"editSticker": "Edit Sticker",
|
||||
"deleteSticker": "Delete Sticker",
|
||||
"deleteStickerHint": "Are you sure to delete this sticker? This action cannot be undone.",
|
||||
"deleteStickerHint": "Are you sure you want to delete this sticker? This action cannot be undone.",
|
||||
"stickerImage": "Image",
|
||||
"stickerSlug": "Slug",
|
||||
"stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.",
|
||||
@@ -333,13 +336,25 @@
|
||||
"levelingProgress": "Leveling Progress",
|
||||
"levelingProgressExperience": "{} EXP",
|
||||
"levelingProgressLevel": "Level {}",
|
||||
"levelingStage1": "Novice",
|
||||
"levelingStage2": "Apprentice",
|
||||
"levelingStage3": "Journeyman",
|
||||
"levelingStage4": "Adept",
|
||||
"levelingStage5": "Expert",
|
||||
"levelingStage6": "Master",
|
||||
"levelingStage7": "Grandmaster",
|
||||
"levelingStage8": "Legend",
|
||||
"levelingStage9": "Myth",
|
||||
"levelingStage10": "Immortal",
|
||||
"levelingStage11": "Divine",
|
||||
"levelingStage12": "Transcendent",
|
||||
"fileUploadingProgress": "Uploading file #{}: {}%",
|
||||
"removeChatMember": "Remove Chat Room Member",
|
||||
"removeChatMemberHint": "Are you sure to remove this member from the room?",
|
||||
"removeChatMemberHint": "Are you sure you want to remove this member from the room?",
|
||||
"removeRealmMember": "Remove Realm Member",
|
||||
"removeRealmMemberHint": "Are you sure to remove this member from the realm?",
|
||||
"removeRealmMemberHint": "Are you sure you want to remove this member from the realm?",
|
||||
"removePublisherMember": "Remove Publisher Member",
|
||||
"removePublisherMemberHint": "Are you sure to remove this member from the publisher?",
|
||||
"removePublisherMemberHint": "Are you sure you want to remove this member from the publisher?",
|
||||
"memberRole": "Member Role",
|
||||
"memberRoleHint": "Greater number has higher permission.",
|
||||
"memberRoleEdit": "Edit role for @{}",
|
||||
@@ -348,9 +363,9 @@
|
||||
"brokenLink": "Unable open link {}... It might be broken or missing uri parts...",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"leaveChatRoom": "Leave Chat Room",
|
||||
"leaveChatRoomHint": "Are you sure to leave this chat room?",
|
||||
"leaveChatRoomHint": "Are you sure you want to leave this chat room?",
|
||||
"leaveRealm": "Leave Realm",
|
||||
"leaveRealmHint": "Are you sure to leave this realm?",
|
||||
"leaveRealmHint": "Are you sure you want to leave this realm?",
|
||||
"walletNotFound": "Wallet not found",
|
||||
"walletCreateHint": "You don't have a wallet yet. Create one to start using the Solar Network eWallet.",
|
||||
"walletCreate": "Create a Wallet",
|
||||
@@ -453,7 +468,7 @@
|
||||
"one": "{} is typing...",
|
||||
"other": "{} are typing..."
|
||||
},
|
||||
"authDeviceEditLabel": "Edit Label",
|
||||
"authDeviceEditLabel": "Edit Device Label",
|
||||
"authDeviceLabelTitle": "Edit Device Label",
|
||||
"authDeviceLabelHint": "Enter a name for this device",
|
||||
"authDeviceSwipeEditHint": "Swipe left to edit label",
|
||||
@@ -470,6 +485,7 @@
|
||||
"settingsKeyboardShortcutSettings": "Settings",
|
||||
"settingsKeyboardShortcutNewMessage": "New Message",
|
||||
"settingsKeyboardShortcutCloseDialog": "Close Dialog",
|
||||
"settingsMessageDisplayStyle": "Message Display Style",
|
||||
"close": "Close",
|
||||
"drafts": "Drafts",
|
||||
"noDrafts": "No drafts yet",
|
||||
@@ -520,7 +536,7 @@
|
||||
"contactMethodPrimary": "Primary",
|
||||
"contactMethodSetPrimary": "Set as Primary",
|
||||
"contactMethodSetPrimaryHint": "Set this contact method as your primary contact method for account recovery and notifications",
|
||||
"contactMethodDeleteHint": "Are you sure to delete this contact method? This action cannot be undone.",
|
||||
"contactMethodDeleteHint": "Are you sure you want to delete this contact method? This action cannot be undone.",
|
||||
"contactMethodMakePublic": "Make Public",
|
||||
"contactMethodMakePrivate": "Make Private",
|
||||
"contactMethodPublic": "Public",
|
||||
@@ -554,6 +570,7 @@
|
||||
"checkInResultT2": "Mid",
|
||||
"checkInResultT3": "Good",
|
||||
"checkInResultT4": "Best",
|
||||
"checkInResultT5": "Birthday",
|
||||
"accountProfileView": "View Profile",
|
||||
"unspecified": "Unspecified",
|
||||
"added": "Added",
|
||||
@@ -646,8 +663,6 @@
|
||||
"abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.",
|
||||
"abuseReportError": "Failed to submit report. Please try again.",
|
||||
"abuseReportReasonRequired": "Please provide details about the issue",
|
||||
"abuseReportSuccessTitle": "Report Submitted",
|
||||
"abuseReportErrorTitle": "Error",
|
||||
"abuseReportTypeSpam": "Spam or Misleading",
|
||||
"abuseReportTypeHarassment": "Harassment or Abuse",
|
||||
"abuseReportTypeInappropriate": "Inappropriate Content",
|
||||
@@ -829,11 +844,6 @@
|
||||
"postCategorySports": "Sports",
|
||||
"postCategoryFinance": "Finance",
|
||||
"postCategoryLife": "Life",
|
||||
"postCategoryArt": "Art",
|
||||
"postCategoryStudy": "Study",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryProgramming": "Programming",
|
||||
"postCategoryMusic": "Music",
|
||||
"links": "Links",
|
||||
"addLink": "Add link",
|
||||
"linkKey": "Link Name",
|
||||
@@ -892,6 +902,15 @@
|
||||
"attachmentOnDevice": "On-device",
|
||||
"attachmentOnCloud": "On-cloud",
|
||||
"attachments": "Attachments",
|
||||
"uploadAttachment": "Upload Attachment",
|
||||
"attachmentPreview": "Attachment Preview",
|
||||
"selectPool": "Select Pool",
|
||||
"choosePool": "Choose a pool",
|
||||
"errorLoadingPools": "Error loading pools",
|
||||
"quotaCostInfo": "This upload will cost {} quota points",
|
||||
"uploadConstraints": "Upload Constraints",
|
||||
"fileSizeExceeded": "File size exceeds the maximum limit of {}",
|
||||
"fileTypeNotAccepted": "File type is not accepted by this pool",
|
||||
"publisherCollabInvitation": "Collabration invitations",
|
||||
"publisherCollabInvitationCount": {
|
||||
"zero": "No invitation",
|
||||
@@ -1008,6 +1027,11 @@
|
||||
"expandPoll": "Expand Poll",
|
||||
"collapsePoll": "Collapse Poll",
|
||||
"embedView": "Embed View",
|
||||
"auto": "Auto",
|
||||
"manual": "Manual",
|
||||
"iframeCode": "Iframe Code",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "Parse Iframe",
|
||||
"embedUri": "Embed URI",
|
||||
"aspectRatio": "Aspect Ratio",
|
||||
"renderer": "Renderer",
|
||||
@@ -1018,5 +1042,26 @@
|
||||
"currentEmbed": "Current Embed",
|
||||
"noEmbed": "No embed yet",
|
||||
"save": "Save",
|
||||
"webView": "Web View"
|
||||
"webView": "Web View",
|
||||
"messageActions": "Message Actions",
|
||||
"viewEmbedLoadHint": "Tap to load",
|
||||
"files": "Files",
|
||||
"confirmDeleteFile": "Are you sure you want to delete this file?",
|
||||
"deleteFile": "Delete File",
|
||||
"failedToDeleteFile": "Failed to delete file",
|
||||
"drive": "Drive",
|
||||
"allPools": "All Pools",
|
||||
"includeRecycled": "Include Recycled",
|
||||
"confirmDeleteRecycledFiles": "Are you sure you want to delete all recycled files?",
|
||||
"deleteRecycledFiles": "Delete Recycled Files",
|
||||
"recycledFilesDeleted": "Recycled files deleted successfully",
|
||||
"failedToDeleteRecycledFiles": "Failed to delete recycled files",
|
||||
"upload": "Upload",
|
||||
"fileInfoTitle": "File Information",
|
||||
"fileHashCopied": "File hash copied to clipboard",
|
||||
"fileIdCopied": "File ID copied to clipboard",
|
||||
"fileNameCopied": "File name copied to clipboard",
|
||||
"fileMetadata": "File Metadata",
|
||||
"userMetadata": "User Metadata",
|
||||
"valueCopied": "Value copied to clipboard"
|
||||
}
|
||||
|
@@ -122,6 +122,9 @@
|
||||
"addVideo": "添加视频",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上传文件",
|
||||
"settingsDefaultPool": "选择文件池",
|
||||
"settingsDefaultPoolHelper": "为文件上传选择一个默认池",
|
||||
"createDirectMessage": "创建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反应",
|
||||
@@ -280,6 +283,18 @@
|
||||
"levelingProgress": "等级进度",
|
||||
"levelingProgressExperience": "{} 经验值",
|
||||
"levelingProgressLevel": "等级 {}",
|
||||
"levelingStage1": "新手",
|
||||
"levelingStage2": "学徒",
|
||||
"levelingStage3": "熟练工",
|
||||
"levelingStage4": "行家",
|
||||
"levelingStage5": "专家",
|
||||
"levelingStage6": "大师",
|
||||
"levelingStage7": "宗师",
|
||||
"levelingStage8": "传奇",
|
||||
"levelingStage9": "神话",
|
||||
"levelingStage10": "不朽",
|
||||
"levelingStage11": "神圣",
|
||||
"levelingStage12": "超凡",
|
||||
"fileUploadingProgress": "正在上传文件 #{}: {}%",
|
||||
"removeChatMember": "移除聊天室成员",
|
||||
"removeChatMemberHint": "确定要将此成员从聊天室中移除吗?",
|
||||
|
@@ -122,6 +122,10 @@
|
||||
"addVideo": "添加視頻",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上傳文件",
|
||||
"settingsDefaultPool": "選擇文件池",
|
||||
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
|
||||
|
||||
"createDirectMessage": "創建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反應",
|
||||
|
BIN
assets/images/stickers/angry.png
Normal file
BIN
assets/images/stickers/angry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/stickers/clap.png
Normal file
BIN
assets/images/stickers/clap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/images/stickers/confuse.png
Normal file
BIN
assets/images/stickers/confuse.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 668 KiB |
BIN
assets/images/stickers/party.png
Normal file
BIN
assets/images/stickers/party.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/stickers/pray.png
Normal file
BIN
assets/images/stickers/pray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 666 KiB |
BIN
assets/images/stickers/thumb_up.png
Normal file
BIN
assets/images/stickers/thumb_up.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 623 KiB |
@@ -5,3 +5,7 @@ targets:
|
||||
options:
|
||||
explicit_to_json: true
|
||||
field_rename: snake
|
||||
drift_dev:
|
||||
options:
|
||||
databases:
|
||||
app_database: lib/database/drift_db.dart
|
||||
|
1
drift_schemas/app_database/drift_schema_v6.json
Normal file
1
drift_schemas/app_database/drift_schema_v6.json
Normal file
File diff suppressed because one or more lines are too long
@@ -50,18 +50,18 @@ PODS:
|
||||
- Firebase/Messaging (12.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.2.0)
|
||||
- firebase_analytics (12.0.1):
|
||||
- firebase_analytics (12.0.2):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.2.0)
|
||||
- Flutter
|
||||
- firebase_core (4.1.0):
|
||||
- firebase_core (4.1.1):
|
||||
- Firebase/CoreOnly (= 12.2.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.0.1):
|
||||
- firebase_crashlytics (5.0.2):
|
||||
- Firebase/Crashlytics (= 12.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.1):
|
||||
- firebase_messaging (16.0.2):
|
||||
- Firebase/Messaging (= 12.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
@@ -476,10 +476,10 @@ SPEC CHECKSUMS:
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
||||
firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae
|
||||
firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302
|
||||
firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f
|
||||
firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c
|
||||
firebase_analytics: 8c78ce6224e0623152379d6cc7ef3d9098477b7e
|
||||
firebase_core: dfc4bd142bee4bc53a5d482397ca322c2dd3165d
|
||||
firebase_crashlytics: e55dcf895eed0dd87c447dd5aff8db7f1bb8bbdb
|
||||
firebase_messaging: 38c66c1184695b0c87abe51d40fc590718abed1a
|
||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
||||
|
@@ -12,7 +12,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 6;
|
||||
int get schemaVersion => 7;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -21,8 +21,8 @@ class AppDatabase extends _$AppDatabase {
|
||||
},
|
||||
onUpgrade: (Migrator m, int from, int to) async {
|
||||
if (from < 2) {
|
||||
// Add isRead column with default value false
|
||||
await m.addColumn(chatMessages, chatMessages.isRead);
|
||||
// Add isDeleted column with default value false
|
||||
await m.addColumn(chatMessages, chatMessages.isDeleted);
|
||||
}
|
||||
if (from < 4) {
|
||||
// Drop old draft tables if they exist
|
||||
@@ -32,6 +32,29 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Migrate from old schema to new schema with separate searchable fields
|
||||
await _migrateToVersion6(m);
|
||||
}
|
||||
if (from < 7) {
|
||||
// Add new columns from SnChatMessage, ignore if they already exist
|
||||
final columnsToAdd = [
|
||||
chatMessages.updatedAt,
|
||||
chatMessages.deletedAt,
|
||||
chatMessages.type,
|
||||
chatMessages.meta,
|
||||
chatMessages.membersMentioned,
|
||||
chatMessages.editedAt,
|
||||
chatMessages.attachments,
|
||||
chatMessages.reactions,
|
||||
chatMessages.repliedMessageId,
|
||||
chatMessages.forwardedMessageId,
|
||||
];
|
||||
|
||||
for (final column in columnsToAdd) {
|
||||
try {
|
||||
await m.addColumn(chatMessages, column);
|
||||
} catch (e) {
|
||||
// Column already exists, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -116,12 +139,6 @@ class AppDatabase extends _$AppDatabase {
|
||||
)).write(ChatMessagesCompanion(status: Value(status)));
|
||||
}
|
||||
|
||||
Future<int> markMessageAsRead(String id) {
|
||||
return (update(chatMessages)..where(
|
||||
(m) => m.id.equals(id),
|
||||
)).write(ChatMessagesCompanion(isRead: const Value(true)));
|
||||
}
|
||||
|
||||
Future<int> deleteMessage(String id) {
|
||||
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
|
||||
}
|
||||
@@ -134,15 +151,27 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
Future<List<LocalChatMessage>> searchMessages(
|
||||
String roomId,
|
||||
String query,
|
||||
) async {
|
||||
String query, {
|
||||
bool? withAttachments,
|
||||
}) async {
|
||||
var selectStatement = select(chatMessages)
|
||||
..where((m) => m.roomId.equals(roomId));
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
final searchTerm = '%$query%';
|
||||
selectStatement =
|
||||
selectStatement
|
||||
..where((m) => m.content.like('%${query.toLowerCase()}%'));
|
||||
selectStatement..where(
|
||||
(m) =>
|
||||
m.content.like(searchTerm) |
|
||||
m.meta.like(searchTerm) |
|
||||
m.attachments.like(searchTerm) |
|
||||
m.type.like(searchTerm),
|
||||
);
|
||||
}
|
||||
|
||||
if (withAttachments == true) {
|
||||
selectStatement =
|
||||
selectStatement..where((m) => m.attachments.equals('[]').not());
|
||||
}
|
||||
|
||||
final messages =
|
||||
@@ -154,16 +183,26 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
// Convert between Drift and model objects
|
||||
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
|
||||
final remote = message.toRemoteMessage();
|
||||
return ChatMessagesCompanion(
|
||||
id: Value(message.id),
|
||||
roomId: Value(message.roomId),
|
||||
senderId: Value(message.senderId),
|
||||
content: Value(message.toRemoteMessage().content),
|
||||
content: Value(remote.content),
|
||||
nonce: Value(message.nonce),
|
||||
data: Value(jsonEncode(message.data)),
|
||||
createdAt: Value(message.createdAt),
|
||||
status: Value(message.status),
|
||||
isRead: Value(message.isRead),
|
||||
updatedAt: Value(remote.updatedAt),
|
||||
deletedAt: Value(remote.deletedAt),
|
||||
type: Value(remote.type),
|
||||
meta: Value(remote.meta),
|
||||
membersMentioned: Value(remote.membersMentioned),
|
||||
editedAt: Value(remote.editedAt),
|
||||
attachments: Value(remote.attachments.map((e) => e.toJson()).toList()),
|
||||
reactions: Value(remote.reactions.map((e) => e.toJson()).toList()),
|
||||
repliedMessageId: Value(remote.repliedMessageId),
|
||||
forwardedMessageId: Value(remote.forwardedMessageId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,7 +216,18 @@ class AppDatabase extends _$AppDatabase {
|
||||
createdAt: dbMessage.createdAt,
|
||||
status: dbMessage.status,
|
||||
nonce: dbMessage.nonce,
|
||||
isRead: dbMessage.isRead,
|
||||
content: dbMessage.content,
|
||||
isDeleted: dbMessage.isDeleted,
|
||||
updatedAt: dbMessage.updatedAt,
|
||||
deletedAt: dbMessage.deletedAt,
|
||||
type: dbMessage.type,
|
||||
meta: dbMessage.meta,
|
||||
membersMentioned: dbMessage.membersMentioned,
|
||||
editedAt: dbMessage.editedAt,
|
||||
attachments: dbMessage.attachments,
|
||||
reactions: dbMessage.reactions,
|
||||
repliedMessageId: dbMessage.repliedMessageId,
|
||||
forwardedMessageId: dbMessage.forwardedMessageId,
|
||||
);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,41 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
|
||||
class MapConverter extends TypeConverter<Map<String, dynamic>, String> {
|
||||
const MapConverter();
|
||||
|
||||
@override
|
||||
Map<String, dynamic> fromSql(String fromDb) => json.decode(fromDb);
|
||||
|
||||
@override
|
||||
String toSql(Map<String, dynamic> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ListStringConverter extends TypeConverter<List<String>, String> {
|
||||
const ListStringConverter();
|
||||
|
||||
@override
|
||||
List<String> fromSql(String fromDb) => List<String>.from(json.decode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(List<String> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ListMapConverter
|
||||
extends TypeConverter<List<Map<String, dynamic>>, String> {
|
||||
const ListMapConverter();
|
||||
|
||||
@override
|
||||
List<Map<String, dynamic>> fromSql(String fromDb) =>
|
||||
List<Map<String, dynamic>>.from(json.decode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
|
||||
}
|
||||
|
||||
class ChatMessages extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get roomId => text()();
|
||||
@@ -11,7 +45,24 @@ class ChatMessages extends Table {
|
||||
TextColumn get data => text()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
IntColumn get status => intEnum<MessageStatus>()();
|
||||
BoolColumn get isRead => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get isDeleted =>
|
||||
boolean().nullable().withDefault(const Constant(false))();
|
||||
DateTimeColumn get updatedAt => dateTime().nullable()();
|
||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||
TextColumn get type => text().withDefault(const Constant('text'))();
|
||||
TextColumn get meta =>
|
||||
text().map(const MapConverter()).withDefault(const Constant('{}'))();
|
||||
TextColumn get membersMentioned =>
|
||||
text()
|
||||
.map(const ListStringConverter())
|
||||
.withDefault(const Constant('[]'))();
|
||||
DateTimeColumn get editedAt => dateTime().nullable()();
|
||||
TextColumn get attachments =>
|
||||
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
||||
TextColumn get reactions =>
|
||||
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
|
||||
TextColumn get repliedMessageId => text().nullable()();
|
||||
TextColumn get forwardedMessageId => text().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
@@ -25,8 +76,19 @@ class LocalChatMessage {
|
||||
final DateTime createdAt;
|
||||
MessageStatus status;
|
||||
final String? nonce;
|
||||
final String? content;
|
||||
final bool? isDeleted;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime? deletedAt;
|
||||
final String type;
|
||||
final Map<String, dynamic> meta;
|
||||
final List<String> membersMentioned;
|
||||
final DateTime? editedAt;
|
||||
final List<Map<String, dynamic>> attachments;
|
||||
final List<Map<String, dynamic>> reactions;
|
||||
final String? repliedMessageId;
|
||||
final String? forwardedMessageId;
|
||||
List<UniversalFile>? localAttachments;
|
||||
bool isRead;
|
||||
|
||||
LocalChatMessage({
|
||||
required this.id,
|
||||
@@ -36,8 +98,19 @@ class LocalChatMessage {
|
||||
required this.createdAt,
|
||||
required this.nonce,
|
||||
required this.status,
|
||||
this.content,
|
||||
this.isDeleted,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.type,
|
||||
required this.meta,
|
||||
required this.membersMentioned,
|
||||
this.editedAt,
|
||||
required this.attachments,
|
||||
required this.reactions,
|
||||
this.repliedMessageId,
|
||||
this.forwardedMessageId,
|
||||
this.localAttachments,
|
||||
this.isRead = false,
|
||||
});
|
||||
|
||||
SnChatMessage toRemoteMessage() {
|
||||
@@ -48,7 +121,6 @@ class LocalChatMessage {
|
||||
SnChatMessage message,
|
||||
MessageStatus status, {
|
||||
String? nonce,
|
||||
bool isRead = false,
|
||||
}) {
|
||||
return LocalChatMessage(
|
||||
id: message.id,
|
||||
@@ -58,7 +130,18 @@ class LocalChatMessage {
|
||||
createdAt: message.createdAt,
|
||||
status: status,
|
||||
nonce: nonce ?? message.nonce,
|
||||
isRead: isRead,
|
||||
content: message.content,
|
||||
isDeleted: false,
|
||||
updatedAt: message.updatedAt,
|
||||
deletedAt: null,
|
||||
type: message.type,
|
||||
meta: message.meta,
|
||||
membersMentioned: message.membersMentioned,
|
||||
editedAt: message.editedAt,
|
||||
attachments: message.attachments.map((e) => e.toJson()).toList(),
|
||||
reactions: message.reactions.map((e) => e.toJson()).toList(),
|
||||
repliedMessageId: message.repliedMessageId,
|
||||
forwardedMessageId: message.forwardedMessageId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -98,6 +98,7 @@ sealed class SnAccountStatus with _$SnAccountStatus {
|
||||
required bool isNotDisturb,
|
||||
required bool isCustomized,
|
||||
@Default("") String label,
|
||||
required Map<String, dynamic>? meta,
|
||||
required DateTime? clearedAt,
|
||||
required String accountId,
|
||||
required DateTime createdAt,
|
||||
|
@@ -1053,7 +1053,7 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
||||
/// @nodoc
|
||||
mixin _$SnAccountStatus {
|
||||
|
||||
String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; Map<String, dynamic>? get meta; DateTime? get clearedAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1066,16 +1066,16 @@ $SnAccountStatusCopyWith<SnAccountStatus> get copyWith => _$SnAccountStatusCopyW
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(meta),clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,7 +1086,7 @@ abstract mixin class $SnAccountStatusCopyWith<$Res> {
|
||||
factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1103,7 +1103,7 @@ class _$SnAccountStatusCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1112,7 +1112,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig
|
||||
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1199,10 +1200,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus() when $default != null:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -1220,10 +1221,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus():
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -1237,10 +1238,10 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountStatus() when $default != null:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.isNotDisturb,_that.isCustomized,_that.label,_that.meta,_that.clearedAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -1252,7 +1253,7 @@ return $default(_that.id,_that.attitude,_that.isOnline,_that.isInvisible,_that.i
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnAccountStatus implements SnAccountStatus {
|
||||
const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required final Map<String, dynamic>? meta, required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta;
|
||||
factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -1262,6 +1263,15 @@ class _SnAccountStatus implements SnAccountStatus {
|
||||
@override final bool isNotDisturb;
|
||||
@override final bool isCustomized;
|
||||
@override@JsonKey() final String label;
|
||||
final Map<String, dynamic>? _meta;
|
||||
@override Map<String, dynamic>? get meta {
|
||||
final value = _meta;
|
||||
if (value == null) return null;
|
||||
if (_meta is EqualUnmodifiableMapView) return _meta;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final DateTime? clearedAt;
|
||||
@override final String accountId;
|
||||
@override final DateTime createdAt;
|
||||
@@ -1281,16 +1291,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,const DeepCollectionEquality().hash(_meta),clearedAt,accountId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, meta: $meta, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -1301,7 +1311,7 @@ abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatus
|
||||
factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, Map<String, dynamic>? meta, DateTime? clearedAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1318,7 +1328,7 @@ class __$SnAccountStatusCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountStatus
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? meta = freezed,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAccountStatus(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1327,7 +1337,8 @@ as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ig
|
||||
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
|
||||
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as String,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -158,6 +158,7 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
|
||||
isNotDisturb: json['is_not_disturb'] as bool,
|
||||
isCustomized: json['is_customized'] as bool,
|
||||
label: json['label'] as String? ?? "",
|
||||
meta: json['meta'] as Map<String, dynamic>?,
|
||||
clearedAt:
|
||||
json['cleared_at'] == null
|
||||
? null
|
||||
@@ -180,6 +181,7 @@ Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
|
||||
'is_not_disturb': instance.isNotDisturb,
|
||||
'is_customized': instance.isCustomized,
|
||||
'label': instance.label,
|
||||
'meta': instance.meta,
|
||||
'cleared_at': instance.clearedAt?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
|
@@ -40,7 +40,7 @@ sealed class SnChatMessage with _$SnChatMessage {
|
||||
String? content,
|
||||
String? nonce,
|
||||
@Default({}) Map<String, dynamic> meta,
|
||||
@Default([]) List<String> membersMetioned,
|
||||
@Default([]) List<String> membersMentioned,
|
||||
DateTime? editedAt,
|
||||
@Default([]) List<SnCloudFile> attachments,
|
||||
@Default([]) List<SnChatReaction> reactions,
|
||||
@@ -117,23 +117,10 @@ class MessageChangeAction {
|
||||
static const String delete = "delete";
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class MessageChange with _$MessageChange {
|
||||
const factory MessageChange({
|
||||
required String messageId,
|
||||
required String action,
|
||||
SnChatMessage? message,
|
||||
required DateTime timestamp,
|
||||
}) = _MessageChange;
|
||||
|
||||
factory MessageChange.fromJson(Map<String, dynamic> json) =>
|
||||
_$MessageChangeFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class MessageSyncResponse with _$MessageSyncResponse {
|
||||
const factory MessageSyncResponse({
|
||||
@Default([]) List<MessageChange> changes,
|
||||
@Default([]) List<SnChatMessage> messages,
|
||||
required DateTime currentTimestamp,
|
||||
}) = _MessageSyncResponse;
|
||||
|
||||
|
@@ -391,7 +391,7 @@ $SnRealmCopyWith<$Res>? get realm {
|
||||
/// @nodoc
|
||||
mixin _$SnChatMessage {
|
||||
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMetioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
|
||||
DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get type; String? get content; String? get nonce; Map<String, dynamic> get meta; List<String> get membersMentioned; DateTime? get editedAt; List<SnCloudFile> get attachments; List<SnChatReaction> get reactions; String? get repliedMessageId; String? get forwardedMessageId; String get senderId; SnChatMember get sender; String get chatRoomId;
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -404,16 +404,16 @@ $SnChatMessageCopyWith<SnChatMessage> get copyWith => _$SnChatMessageCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMetioned, membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other.meta, meta)&&const DeepCollectionEquality().equals(other.membersMentioned, membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMetioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(meta),const DeepCollectionEquality().hash(membersMentioned),editedAt,const DeepCollectionEquality().hash(attachments),const DeepCollectionEquality().hash(reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
}
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ abstract mixin class $SnChatMessageCopyWith<$Res> {
|
||||
factory $SnChatMessageCopyWith(SnChatMessage value, $Res Function(SnChatMessage) _then) = _$SnChatMessageCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
});
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ class _$SnChatMessageCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -451,7 +451,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
|
||||
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: null == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self.membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self.membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnCloudFile>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
@@ -551,10 +551,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage() when $default != null:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -572,10 +572,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage():
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);}
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -589,10 +589,10 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnChatMessage() when $default != null:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMetioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.type,_that.content,_that.nonce,_that.meta,_that.membersMentioned,_that.editedAt,_that.attachments,_that.reactions,_that.repliedMessageId,_that.forwardedMessageId,_that.senderId,_that.sender,_that.chatRoomId);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -604,7 +604,7 @@ return $default(_that.createdAt,_that.updatedAt,_that.deletedAt,_that.id,_that.t
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnChatMessage implements SnChatMessage {
|
||||
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMetioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMetioned = membersMetioned,_attachments = attachments,_reactions = reactions;
|
||||
const _SnChatMessage({required this.createdAt, required this.updatedAt, this.deletedAt, required this.id, this.type = 'text', this.content, this.nonce, final Map<String, dynamic> meta = const {}, final List<String> membersMentioned = const [], this.editedAt, final List<SnCloudFile> attachments = const [], final List<SnChatReaction> reactions = const [], this.repliedMessageId, this.forwardedMessageId, required this.senderId, required this.sender, required this.chatRoomId}): _meta = meta,_membersMentioned = membersMentioned,_attachments = attachments,_reactions = reactions;
|
||||
factory _SnChatMessage.fromJson(Map<String, dynamic> json) => _$SnChatMessageFromJson(json);
|
||||
|
||||
@override final DateTime createdAt;
|
||||
@@ -621,11 +621,11 @@ class _SnChatMessage implements SnChatMessage {
|
||||
return EqualUnmodifiableMapView(_meta);
|
||||
}
|
||||
|
||||
final List<String> _membersMetioned;
|
||||
@override@JsonKey() List<String> get membersMetioned {
|
||||
if (_membersMetioned is EqualUnmodifiableListView) return _membersMetioned;
|
||||
final List<String> _membersMentioned;
|
||||
@override@JsonKey() List<String> get membersMentioned {
|
||||
if (_membersMentioned is EqualUnmodifiableListView) return _membersMentioned;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_membersMetioned);
|
||||
return EqualUnmodifiableListView(_membersMentioned);
|
||||
}
|
||||
|
||||
@override final DateTime? editedAt;
|
||||
@@ -662,16 +662,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMetioned, _membersMetioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatMessage&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.content, content) || other.content == content)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&const DeepCollectionEquality().equals(other._meta, _meta)&&const DeepCollectionEquality().equals(other._membersMentioned, _membersMentioned)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&(identical(other.repliedMessageId, repliedMessageId) || other.repliedMessageId == repliedMessageId)&&(identical(other.forwardedMessageId, forwardedMessageId) || other.forwardedMessageId == forwardedMessageId)&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.sender, sender) || other.sender == sender)&&(identical(other.chatRoomId, chatRoomId) || other.chatRoomId == chatRoomId));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMetioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
int get hashCode => Object.hash(runtimeType,createdAt,updatedAt,deletedAt,id,type,content,nonce,const DeepCollectionEquality().hash(_meta),const DeepCollectionEquality().hash(_membersMentioned),editedAt,const DeepCollectionEquality().hash(_attachments),const DeepCollectionEquality().hash(_reactions),repliedMessageId,forwardedMessageId,senderId,sender,chatRoomId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMetioned: $membersMetioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
return 'SnChatMessage(createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, id: $id, type: $type, content: $content, nonce: $nonce, meta: $meta, membersMentioned: $membersMentioned, editedAt: $editedAt, attachments: $attachments, reactions: $reactions, repliedMessageId: $repliedMessageId, forwardedMessageId: $forwardedMessageId, senderId: $senderId, sender: $sender, chatRoomId: $chatRoomId)';
|
||||
}
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ abstract mixin class _$SnChatMessageCopyWith<$Res> implements $SnChatMessageCopy
|
||||
factory _$SnChatMessageCopyWith(_SnChatMessage value, $Res Function(_SnChatMessage) _then) = __$SnChatMessageCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMetioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, String type, String? content, String? nonce, Map<String, dynamic> meta, List<String> membersMentioned, DateTime? editedAt, List<SnCloudFile> attachments, List<SnChatReaction> reactions, String? repliedMessageId, String? forwardedMessageId, String senderId, SnChatMember sender, String chatRoomId
|
||||
});
|
||||
|
||||
|
||||
@@ -699,7 +699,7 @@ class __$SnChatMessageCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnChatMessage
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMetioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? id = null,Object? type = null,Object? content = freezed,Object? nonce = freezed,Object? meta = null,Object? membersMentioned = null,Object? editedAt = freezed,Object? attachments = null,Object? reactions = null,Object? repliedMessageId = freezed,Object? forwardedMessageId = freezed,Object? senderId = null,Object? sender = null,Object? chatRoomId = null,}) {
|
||||
return _then(_SnChatMessage(
|
||||
createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -709,7 +709,7 @@ as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non
|
||||
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: null == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMetioned: null == membersMetioned ? _self._membersMetioned : membersMetioned // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,membersMentioned: null == membersMentioned ? _self._membersMentioned : membersMentioned // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnCloudFile>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1691,300 +1691,10 @@ $SnChatMessageCopyWith<$Res>? get lastMessage {
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MessageChange {
|
||||
|
||||
String get messageId; String get action; SnChatMessage? get message; DateTime get timestamp;
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MessageChangeCopyWith<MessageChange> get copyWith => _$MessageChangeCopyWithImpl<MessageChange>(this as MessageChange, _$identity);
|
||||
|
||||
/// Serializes this MessageChange to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MessageChangeCopyWith<$Res> {
|
||||
factory $MessageChangeCopyWith(MessageChange value, $Res Function(MessageChange) _then) = _$MessageChangeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String messageId, String action, SnChatMessage? message, DateTime timestamp
|
||||
});
|
||||
|
||||
|
||||
$SnChatMessageCopyWith<$Res>? get message;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$MessageChangeCopyWithImpl<$Res>
|
||||
implements $MessageChangeCopyWith<$Res> {
|
||||
_$MessageChangeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MessageChange _self;
|
||||
final $Res Function(MessageChange) _then;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
|
||||
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatMessageCopyWith<$Res>? get message {
|
||||
if (_self.message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
|
||||
return _then(_self.copyWith(message: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [MessageChange].
|
||||
extension MessageChangePatterns on MessageChange {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _MessageChange value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _MessageChange value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _MessageChange value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String messageId, String action, SnChatMessage? message, DateTime timestamp) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange():
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String messageId, String action, SnChatMessage? message, DateTime timestamp)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageChange() when $default != null:
|
||||
return $default(_that.messageId,_that.action,_that.message,_that.timestamp);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _MessageChange implements MessageChange {
|
||||
const _MessageChange({required this.messageId, required this.action, this.message, required this.timestamp});
|
||||
factory _MessageChange.fromJson(Map<String, dynamic> json) => _$MessageChangeFromJson(json);
|
||||
|
||||
@override final String messageId;
|
||||
@override final String action;
|
||||
@override final SnChatMessage? message;
|
||||
@override final DateTime timestamp;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MessageChangeCopyWith<_MessageChange> get copyWith => __$MessageChangeCopyWithImpl<_MessageChange>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MessageChangeToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageChange&&(identical(other.messageId, messageId) || other.messageId == messageId)&&(identical(other.action, action) || other.action == action)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,messageId,action,message,timestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageChange(messageId: $messageId, action: $action, message: $message, timestamp: $timestamp)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MessageChangeCopyWith<$Res> implements $MessageChangeCopyWith<$Res> {
|
||||
factory _$MessageChangeCopyWith(_MessageChange value, $Res Function(_MessageChange) _then) = __$MessageChangeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String messageId, String action, SnChatMessage? message, DateTime timestamp
|
||||
});
|
||||
|
||||
|
||||
@override $SnChatMessageCopyWith<$Res>? get message;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$MessageChangeCopyWithImpl<$Res>
|
||||
implements _$MessageChangeCopyWith<$Res> {
|
||||
__$MessageChangeCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MessageChange _self;
|
||||
final $Res Function(_MessageChange) _then;
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? messageId = null,Object? action = null,Object? message = freezed,Object? timestamp = null,}) {
|
||||
return _then(_MessageChange(
|
||||
messageId: null == messageId ? _self.messageId : messageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,action: null == action ? _self.action : action // ignore: cast_nullable_to_non_nullable
|
||||
as String,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as SnChatMessage?,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of MessageChange
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnChatMessageCopyWith<$Res>? get message {
|
||||
if (_self.message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnChatMessageCopyWith<$Res>(_self.message!, (value) {
|
||||
return _then(_self.copyWith(message: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MessageSyncResponse {
|
||||
|
||||
List<MessageChange> get changes; DateTime get currentTimestamp;
|
||||
List<SnChatMessage> get messages; DateTime get currentTimestamp;
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -1997,16 +1707,16 @@ $MessageSyncResponseCopyWith<MessageSyncResponse> get copyWith => _$MessageSyncR
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.changes, changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is MessageSyncResponse&&const DeepCollectionEquality().equals(other.messages, messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(changes),currentTimestamp);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(messages),currentTimestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)';
|
||||
return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
|
||||
}
|
||||
|
||||
|
||||
@@ -2017,7 +1727,7 @@ abstract mixin class $MessageSyncResponseCopyWith<$Res> {
|
||||
factory $MessageSyncResponseCopyWith(MessageSyncResponse value, $Res Function(MessageSyncResponse) _then) = _$MessageSyncResponseCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
List<MessageChange> changes, DateTime currentTimestamp
|
||||
List<SnChatMessage> messages, DateTime currentTimestamp
|
||||
});
|
||||
|
||||
|
||||
@@ -2034,10 +1744,10 @@ class _$MessageSyncResponseCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? changes = null,Object? currentTimestamp = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
changes: null == changes ? _self.changes : changes // ignore: cast_nullable_to_non_nullable
|
||||
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
messages: null == messages ? _self.messages : messages // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
@@ -2120,10 +1830,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse() when $default != null:
|
||||
return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
return $default(_that.messages,_that.currentTimestamp);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -2141,10 +1851,10 @@ return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<MessageChange> changes, DateTime currentTimestamp) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<SnChatMessage> messages, DateTime currentTimestamp) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse():
|
||||
return $default(_that.changes,_that.currentTimestamp);}
|
||||
return $default(_that.messages,_that.currentTimestamp);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -2158,10 +1868,10 @@ return $default(_that.changes,_that.currentTimestamp);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<MessageChange> changes, DateTime currentTimestamp)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<SnChatMessage> messages, DateTime currentTimestamp)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _MessageSyncResponse() when $default != null:
|
||||
return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
return $default(_that.messages,_that.currentTimestamp);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -2173,14 +1883,14 @@ return $default(_that.changes,_that.currentTimestamp);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _MessageSyncResponse implements MessageSyncResponse {
|
||||
const _MessageSyncResponse({final List<MessageChange> changes = const [], required this.currentTimestamp}): _changes = changes;
|
||||
const _MessageSyncResponse({final List<SnChatMessage> messages = const [], required this.currentTimestamp}): _messages = messages;
|
||||
factory _MessageSyncResponse.fromJson(Map<String, dynamic> json) => _$MessageSyncResponseFromJson(json);
|
||||
|
||||
final List<MessageChange> _changes;
|
||||
@override@JsonKey() List<MessageChange> get changes {
|
||||
if (_changes is EqualUnmodifiableListView) return _changes;
|
||||
final List<SnChatMessage> _messages;
|
||||
@override@JsonKey() List<SnChatMessage> get messages {
|
||||
if (_messages is EqualUnmodifiableListView) return _messages;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_changes);
|
||||
return EqualUnmodifiableListView(_messages);
|
||||
}
|
||||
|
||||
@override final DateTime currentTimestamp;
|
||||
@@ -2198,16 +1908,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._changes, _changes)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _MessageSyncResponse&&const DeepCollectionEquality().equals(other._messages, _messages)&&(identical(other.currentTimestamp, currentTimestamp) || other.currentTimestamp == currentTimestamp));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_changes),currentTimestamp);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_messages),currentTimestamp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MessageSyncResponse(changes: $changes, currentTimestamp: $currentTimestamp)';
|
||||
return 'MessageSyncResponse(messages: $messages, currentTimestamp: $currentTimestamp)';
|
||||
}
|
||||
|
||||
|
||||
@@ -2218,7 +1928,7 @@ abstract mixin class _$MessageSyncResponseCopyWith<$Res> implements $MessageSync
|
||||
factory _$MessageSyncResponseCopyWith(_MessageSyncResponse value, $Res Function(_MessageSyncResponse) _then) = __$MessageSyncResponseCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
List<MessageChange> changes, DateTime currentTimestamp
|
||||
List<SnChatMessage> messages, DateTime currentTimestamp
|
||||
});
|
||||
|
||||
|
||||
@@ -2235,10 +1945,10 @@ class __$MessageSyncResponseCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of MessageSyncResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? changes = null,Object? currentTimestamp = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? messages = null,Object? currentTimestamp = null,}) {
|
||||
return _then(_MessageSyncResponse(
|
||||
changes: null == changes ? _self._changes : changes // ignore: cast_nullable_to_non_nullable
|
||||
as List<MessageChange>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
messages: null == messages ? _self._messages : messages // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnChatMessage>,currentTimestamp: null == currentTimestamp ? _self.currentTimestamp : currentTimestamp // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
@@ -69,8 +69,8 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
|
||||
content: json['content'] as String?,
|
||||
nonce: json['nonce'] as String?,
|
||||
meta: json['meta'] as Map<String, dynamic>? ?? const {},
|
||||
membersMetioned:
|
||||
(json['members_metioned'] as List<dynamic>?)
|
||||
membersMentioned:
|
||||
(json['members_mentioned'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
@@ -105,7 +105,7 @@ Map<String, dynamic> _$SnChatMessageToJson(_SnChatMessage instance) =>
|
||||
'content': instance.content,
|
||||
'nonce': instance.nonce,
|
||||
'meta': instance.meta,
|
||||
'members_metioned': instance.membersMetioned,
|
||||
'members_mentioned': instance.membersMentioned,
|
||||
'edited_at': instance.editedAt?.toIso8601String(),
|
||||
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
|
||||
'reactions': instance.reactions.map((e) => e.toJson()).toList(),
|
||||
@@ -227,30 +227,11 @@ Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
|
||||
'last_message': instance.lastMessage?.toJson(),
|
||||
};
|
||||
|
||||
_MessageChange _$MessageChangeFromJson(Map<String, dynamic> json) =>
|
||||
_MessageChange(
|
||||
messageId: json['message_id'] as String,
|
||||
action: json['action'] as String,
|
||||
message:
|
||||
json['message'] == null
|
||||
? null
|
||||
: SnChatMessage.fromJson(json['message'] as Map<String, dynamic>),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MessageChangeToJson(_MessageChange instance) =>
|
||||
<String, dynamic>{
|
||||
'message_id': instance.messageId,
|
||||
'action': instance.action,
|
||||
'message': instance.message?.toJson(),
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
_MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
|
||||
_MessageSyncResponse(
|
||||
changes:
|
||||
(json['changes'] as List<dynamic>?)
|
||||
?.map((e) => MessageChange.fromJson(e as Map<String, dynamic>))
|
||||
messages:
|
||||
(json['messages'] as List<dynamic>?)
|
||||
?.map((e) => SnChatMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
currentTimestamp: DateTime.parse(json['current_timestamp'] as String),
|
||||
@@ -259,7 +240,7 @@ _MessageSyncResponse _$MessageSyncResponseFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$MessageSyncResponseToJson(
|
||||
_MessageSyncResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'changes': instance.changes.map((e) => e.toJson()).toList(),
|
||||
'messages': instance.messages.map((e) => e.toJson()).toList(),
|
||||
'current_timestamp': instance.currentTimestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
|
||||
part 'file.freezed.dart';
|
||||
part 'file.g.dart';
|
||||
@@ -42,6 +43,7 @@ sealed class SnCloudFile with _$SnCloudFile {
|
||||
required String? description,
|
||||
required Map<String, dynamic>? fileMeta,
|
||||
required Map<String, dynamic>? userMeta,
|
||||
required SnFilePool? pool,
|
||||
@Default([]) List<int> sensitiveMarks,
|
||||
required String? mimeType,
|
||||
required String? hash,
|
||||
|
@@ -278,7 +278,7 @@ as bool,
|
||||
/// @nodoc
|
||||
mixin _$SnCloudFile {
|
||||
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; SnFilePool? get pool; List<int> get sensitiveMarks; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -291,16 +291,16 @@ $SnCloudFileCopyWith<SnCloudFile> get copyWith => _$SnCloudFileCopyWithImpl<SnCl
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.fileMeta, fileMeta)&&const DeepCollectionEquality().equals(other.userMeta, userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other.sensitiveMarks, sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(fileMeta),const DeepCollectionEquality().hash(userMeta),pool,const DeepCollectionEquality().hash(sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -311,11 +311,11 @@ abstract mixin class $SnCloudFileCopyWith<$Res> {
|
||||
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
$SnFilePoolCopyWith<$Res>? get pool;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -328,14 +328,15 @@ class _$SnCloudFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,fileMeta: freezed == fileMeta ? _self.fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self.sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
|
||||
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
@@ -347,7 +348,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<$Res>? get pool {
|
||||
if (_self.pool == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
|
||||
return _then(_self.copyWith(pool: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -426,10 +439,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -447,10 +460,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile():
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -464,10 +477,10 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnCloudFile() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userMeta,_that.pool,_that.sensitiveMarks,_that.mimeType,_that.hash,_that.size,_that.uploadedAt,_that.uploadedTo,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -479,7 +492,7 @@ return $default(_that.id,_that.name,_that.description,_that.fileMeta,_that.userM
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnCloudFile implements SnCloudFile {
|
||||
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
|
||||
const _SnCloudFile({required this.id, required this.name, required this.description, required final Map<String, dynamic>? fileMeta, required final Map<String, dynamic>? userMeta, required this.pool, final List<int> sensitiveMarks = const [], required this.mimeType, required this.hash, required this.size, required this.uploadedAt, required this.uploadedTo, required this.createdAt, required this.updatedAt, required this.deletedAt}): _fileMeta = fileMeta,_userMeta = userMeta,_sensitiveMarks = sensitiveMarks;
|
||||
factory _SnCloudFile.fromJson(Map<String, dynamic> json) => _$SnCloudFileFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -503,6 +516,7 @@ class _SnCloudFile implements SnCloudFile {
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final SnFilePool? pool;
|
||||
final List<int> _sensitiveMarks;
|
||||
@override@JsonKey() List<int> get sensitiveMarks {
|
||||
if (_sensitiveMarks is EqualUnmodifiableListView) return _sensitiveMarks;
|
||||
@@ -532,16 +546,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnCloudFile&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._fileMeta, _fileMeta)&&const DeepCollectionEquality().equals(other._userMeta, _userMeta)&&(identical(other.pool, pool) || other.pool == pool)&&const DeepCollectionEquality().equals(other._sensitiveMarks, _sensitiveMarks)&&(identical(other.mimeType, mimeType) || other.mimeType == mimeType)&&(identical(other.hash, hash) || other.hash == hash)&&(identical(other.size, size) || other.size == size)&&(identical(other.uploadedAt, uploadedAt) || other.uploadedAt == uploadedAt)&&(identical(other.uploadedTo, uploadedTo) || other.uploadedTo == uploadedTo)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_fileMeta),const DeepCollectionEquality().hash(_userMeta),pool,const DeepCollectionEquality().hash(_sensitiveMarks),mimeType,hash,size,uploadedAt,uploadedTo,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnCloudFile(id: $id, name: $name, description: $description, fileMeta: $fileMeta, userMeta: $userMeta, pool: $pool, sensitiveMarks: $sensitiveMarks, mimeType: $mimeType, hash: $hash, size: $size, uploadedAt: $uploadedAt, uploadedTo: $uploadedTo, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -552,11 +566,11 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith
|
||||
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, SnFilePool? pool, List<int> sensitiveMarks, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $SnFilePoolCopyWith<$Res>? get pool;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -569,14 +583,15 @@ class __$SnCloudFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? pool = freezed,Object? sensitiveMarks = null,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnCloudFile(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,fileMeta: freezed == fileMeta ? _self._fileMeta : fileMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userMeta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,pool: freezed == pool ? _self.pool : pool // ignore: cast_nullable_to_non_nullable
|
||||
as SnFilePool?,sensitiveMarks: null == sensitiveMarks ? _self._sensitiveMarks : sensitiveMarks // ignore: cast_nullable_to_non_nullable
|
||||
as List<int>,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
|
||||
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
@@ -589,7 +604,19 @@ as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnCloudFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<$Res>? get pool {
|
||||
if (_self.pool == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnFilePoolCopyWith<$Res>(_self.pool!, (value) {
|
||||
return _then(_self.copyWith(pool: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@@ -33,6 +33,10 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
|
||||
description: json['description'] as String?,
|
||||
fileMeta: json['file_meta'] as Map<String, dynamic>?,
|
||||
userMeta: json['user_meta'] as Map<String, dynamic>?,
|
||||
pool:
|
||||
json['pool'] == null
|
||||
? null
|
||||
: SnFilePool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||
sensitiveMarks:
|
||||
(json['sensitive_marks'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
@@ -61,6 +65,7 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
||||
'description': instance.description,
|
||||
'file_meta': instance.fileMeta,
|
||||
'user_meta': instance.userMeta,
|
||||
'pool': instance.pool?.toJson(),
|
||||
'sensitive_marks': instance.sensitiveMarks,
|
||||
'mime_type': instance.mimeType,
|
||||
'hash': instance.hash,
|
||||
|
25
lib/models/file_pool.dart
Normal file
25
lib/models/file_pool.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'file_pool.freezed.dart';
|
||||
part 'file_pool.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnFilePool with _$SnFilePool {
|
||||
const factory SnFilePool({
|
||||
required String id,
|
||||
required String name,
|
||||
String? description,
|
||||
Map<String, dynamic>? storageConfig,
|
||||
Map<String, dynamic>? billingConfig,
|
||||
Map<String, dynamic>? policyConfig,
|
||||
bool? isHidden,
|
||||
String? accountId,
|
||||
String? resourceIdentifier,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? deletedAt,
|
||||
}) = _SnFilePool;
|
||||
|
||||
factory SnFilePool.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFilePoolFromJson(json);
|
||||
}
|
328
lib/models/file_pool.freezed.dart
Normal file
328
lib/models/file_pool.freezed.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// 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 'file_pool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFilePool {
|
||||
|
||||
String get id; String get name; String? get description; Map<String, dynamic>? get storageConfig; Map<String, dynamic>? get billingConfig; Map<String, dynamic>? get policyConfig; bool? get isHidden; String? get accountId; String? get resourceIdentifier; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFilePoolCopyWith<SnFilePool> get copyWith => _$SnFilePoolCopyWithImpl<SnFilePool>(this as SnFilePool, _$identity);
|
||||
|
||||
/// Serializes this SnFilePool to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.storageConfig, storageConfig)&&const DeepCollectionEquality().equals(other.billingConfig, billingConfig)&&const DeepCollectionEquality().equals(other.policyConfig, policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(storageConfig),const DeepCollectionEquality().hash(billingConfig),const DeepCollectionEquality().hash(policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFilePoolCopyWith<$Res> {
|
||||
factory $SnFilePoolCopyWith(SnFilePool value, $Res Function(SnFilePool) _then) = _$SnFilePoolCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnFilePoolCopyWithImpl<$Res>
|
||||
implements $SnFilePoolCopyWith<$Res> {
|
||||
_$SnFilePoolCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFilePool _self;
|
||||
final $Res Function(SnFilePool) _then;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,storageConfig: freezed == storageConfig ? _self.storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self.billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self.policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnFilePool].
|
||||
extension SnFilePoolPatterns on SnFilePool {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFilePool value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFilePool value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFilePool value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool():
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFilePool() when $default != null:
|
||||
return $default(_that.id,_that.name,_that.description,_that.storageConfig,_that.billingConfig,_that.policyConfig,_that.isHidden,_that.accountId,_that.resourceIdentifier,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnFilePool implements SnFilePool {
|
||||
const _SnFilePool({required this.id, required this.name, this.description, final Map<String, dynamic>? storageConfig, final Map<String, dynamic>? billingConfig, final Map<String, dynamic>? policyConfig, this.isHidden, this.accountId, this.resourceIdentifier, this.createdAt, this.updatedAt, this.deletedAt}): _storageConfig = storageConfig,_billingConfig = billingConfig,_policyConfig = policyConfig;
|
||||
factory _SnFilePool.fromJson(Map<String, dynamic> json) => _$SnFilePoolFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
@override final String? description;
|
||||
final Map<String, dynamic>? _storageConfig;
|
||||
@override Map<String, dynamic>? get storageConfig {
|
||||
final value = _storageConfig;
|
||||
if (value == null) return null;
|
||||
if (_storageConfig is EqualUnmodifiableMapView) return _storageConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
final Map<String, dynamic>? _billingConfig;
|
||||
@override Map<String, dynamic>? get billingConfig {
|
||||
final value = _billingConfig;
|
||||
if (value == null) return null;
|
||||
if (_billingConfig is EqualUnmodifiableMapView) return _billingConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
final Map<String, dynamic>? _policyConfig;
|
||||
@override Map<String, dynamic>? get policyConfig {
|
||||
final value = _policyConfig;
|
||||
if (value == null) return null;
|
||||
if (_policyConfig is EqualUnmodifiableMapView) return _policyConfig;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final bool? isHidden;
|
||||
@override final String? accountId;
|
||||
@override final String? resourceIdentifier;
|
||||
@override final DateTime? createdAt;
|
||||
@override final DateTime? updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFilePoolCopyWith<_SnFilePool> get copyWith => __$SnFilePoolCopyWithImpl<_SnFilePool>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFilePoolToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFilePool&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._storageConfig, _storageConfig)&&const DeepCollectionEquality().equals(other._billingConfig, _billingConfig)&&const DeepCollectionEquality().equals(other._policyConfig, _policyConfig)&&(identical(other.isHidden, isHidden) || other.isHidden == isHidden)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,const DeepCollectionEquality().hash(_storageConfig),const DeepCollectionEquality().hash(_billingConfig),const DeepCollectionEquality().hash(_policyConfig),isHidden,accountId,resourceIdentifier,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFilePool(id: $id, name: $name, description: $description, storageConfig: $storageConfig, billingConfig: $billingConfig, policyConfig: $policyConfig, isHidden: $isHidden, accountId: $accountId, resourceIdentifier: $resourceIdentifier, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFilePoolCopyWith<$Res> implements $SnFilePoolCopyWith<$Res> {
|
||||
factory _$SnFilePoolCopyWith(_SnFilePool value, $Res Function(_SnFilePool) _then) = __$SnFilePoolCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String? description, Map<String, dynamic>? storageConfig, Map<String, dynamic>? billingConfig, Map<String, dynamic>? policyConfig, bool? isHidden, String? accountId, String? resourceIdentifier, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnFilePoolCopyWithImpl<$Res>
|
||||
implements _$SnFilePoolCopyWith<$Res> {
|
||||
__$SnFilePoolCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFilePool _self;
|
||||
final $Res Function(_SnFilePool) _then;
|
||||
|
||||
/// Create a copy of SnFilePool
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? storageConfig = freezed,Object? billingConfig = freezed,Object? policyConfig = freezed,Object? isHidden = freezed,Object? accountId = freezed,Object? resourceIdentifier = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnFilePool(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,storageConfig: freezed == storageConfig ? _self._storageConfig : storageConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,billingConfig: freezed == billingConfig ? _self._billingConfig : billingConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,policyConfig: freezed == policyConfig ? _self._policyConfig : policyConfig // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,isHidden: freezed == isHidden ? _self.isHidden : isHidden // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,resourceIdentifier: freezed == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable
|
||||
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
47
lib/models/file_pool.g.dart
Normal file
47
lib/models/file_pool.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_pool.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnFilePool _$SnFilePoolFromJson(Map<String, dynamic> json) => _SnFilePool(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
storageConfig: json['storage_config'] as Map<String, dynamic>?,
|
||||
billingConfig: json['billing_config'] as Map<String, dynamic>?,
|
||||
policyConfig: json['policy_config'] as Map<String, dynamic>?,
|
||||
isHidden: json['is_hidden'] as bool?,
|
||||
accountId: json['account_id'] as String?,
|
||||
resourceIdentifier: json['resource_identifier'] as String?,
|
||||
createdAt:
|
||||
json['created_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt:
|
||||
json['updated_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'storage_config': instance.storageConfig,
|
||||
'billing_config': instance.billingConfig,
|
||||
'policy_config': instance.policyConfig,
|
||||
'is_hidden': instance.isHidden,
|
||||
'account_id': instance.accountId,
|
||||
'resource_identifier': instance.resourceIdentifier,
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
@@ -4,7 +4,9 @@ import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/account/status.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
@@ -339,7 +341,9 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
state = state.copyWith(status: 'Server failed: $e');
|
||||
}
|
||||
} else {
|
||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||
Future(() {
|
||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,12 +392,35 @@ final rpcServerStateProvider =
|
||||
'message': (socket, dynamic data) async {
|
||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||
notifier.addActivity(
|
||||
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
||||
'Activity: ${data['args']['activity']['details'] ?? ''}',
|
||||
);
|
||||
final label = data['args']['activity']['details'] ?? 'Unknown';
|
||||
final label = data['args']['activity']['details'] ?? '';
|
||||
final appId = socket.clientId;
|
||||
final meta = data['args']['activity'];
|
||||
try {
|
||||
await setRemoteActivityStatus(ref, label, appId);
|
||||
await setRemoteActivityStatus(
|
||||
ref,
|
||||
label,
|
||||
appId,
|
||||
meta,
|
||||
);
|
||||
final now = DateTime.now();
|
||||
final status = SnAccountStatus(
|
||||
id: 'local_$appId',
|
||||
attitude: 0,
|
||||
isOnline: true,
|
||||
isInvisible: false,
|
||||
isNotDisturb: false,
|
||||
isCustomized: true,
|
||||
label: label,
|
||||
meta: meta,
|
||||
clearedAt: null,
|
||||
accountId: 'me',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
);
|
||||
ref.read(currentAccountStatusProvider.notifier).setStatus(status);
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to set remote activity status: $e',
|
||||
@@ -413,6 +440,7 @@ final rpcServerStateProvider =
|
||||
final appId = socket.clientId;
|
||||
try {
|
||||
await unsetRemoteActivityStatus(ref, appId);
|
||||
ref.read(currentAccountStatusProvider.notifier).clearStatus();
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to unset remote activity status: $e',
|
||||
@@ -433,6 +461,7 @@ Future<void> setRemoteActivityStatus(
|
||||
Ref ref,
|
||||
String label,
|
||||
String appId,
|
||||
Map<String, dynamic> meta,
|
||||
) async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post(
|
||||
@@ -443,6 +472,7 @@ Future<void> setRemoteActivityStatus(
|
||||
'is_automated': true,
|
||||
'label': label,
|
||||
'app_identifier': appId,
|
||||
'meta': meta,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
34
lib/pods/chat_rooms.dart
Normal file
34
lib/pods/chat_rooms.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import "dart:async";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
|
||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||
final controller = StreamController<AppLifecycleState>();
|
||||
|
||||
final observer = _AppLifecycleObserver((state) {
|
||||
if (controller.isClosed) return;
|
||||
controller.add(state);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
||||
ref.onDispose(() {
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
controller.close();
|
||||
});
|
||||
|
||||
return controller.stream;
|
||||
});
|
||||
|
||||
class _AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
final ValueChanged<AppLifecycleState> onChange;
|
||||
_AppLifecycleObserver(this.onChange);
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onChange(state);
|
||||
}
|
||||
}
|
@@ -25,6 +25,8 @@ const kAppSoundEffects = 'app_sound_effects';
|
||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||
const kAppWindowSize = 'app_window_size';
|
||||
const kAppEnterToSend = 'app_enter_to_send';
|
||||
const kAppDefaultPoolId = 'app_default_pool_id';
|
||||
const kAppMessageDisplayStyle = 'app_message_display_style';
|
||||
const kFeaturedPostsCollapsedId =
|
||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||
|
||||
@@ -65,6 +67,8 @@ sealed class AppSettings with _$AppSettings {
|
||||
required String? customFonts,
|
||||
required int? appColorScheme, // The color stored via the int type
|
||||
required Size? windowSize, // The window size for desktop platforms
|
||||
required String? defaultPoolId,
|
||||
required String messageDisplayStyle,
|
||||
}) = _AppSettings;
|
||||
}
|
||||
|
||||
@@ -84,6 +88,8 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
customFonts: prefs.getString(kAppCustomFonts),
|
||||
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
||||
windowSize: _getWindowSizeFromPrefs(prefs),
|
||||
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
||||
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,13 +110,23 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
void setDefaultPoolId(String? value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
if (value != null) {
|
||||
prefs.setString(kAppDefaultPoolId, value);
|
||||
} else {
|
||||
prefs.remove(kAppDefaultPoolId);
|
||||
}
|
||||
state = state.copyWith(defaultPoolId: value);
|
||||
}
|
||||
|
||||
void setAutoTranslate(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppAutoTranslate, value);
|
||||
state = state.copyWith(autoTranslate: value);
|
||||
}
|
||||
|
||||
void setDataSavingMode(bool value){
|
||||
void setDataSavingMode(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppDataSavingMode, value);
|
||||
state = state.copyWith(dataSavingMode: value);
|
||||
@@ -174,6 +190,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
Size? getWindowSize() {
|
||||
return state.windowSize;
|
||||
}
|
||||
|
||||
void setMessageDisplayStyle(String value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setString(kAppMessageDisplayStyle, value);
|
||||
state = state.copyWith(messageDisplayStyle: value);
|
||||
}
|
||||
}
|
||||
|
||||
final updateInfoProvider =
|
||||
|
@@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
|
||||
mixin _$AppSettings {
|
||||
|
||||
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
|
||||
Size? get windowSize;
|
||||
Size? get windowSize;// The window size for desktop platforms
|
||||
String? get defaultPoolId; String get messageDisplayStyle;
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -26,16 +27,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +47,7 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
|
||||
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +64,7 @@ class _$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -75,7 +76,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,
|
||||
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -157,10 +160,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -178,10 +181,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings():
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);}
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -195,10 +198,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -210,7 +213,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
|
||||
|
||||
class _AppSettings implements AppSettings {
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize});
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId, required this.messageDisplayStyle});
|
||||
|
||||
|
||||
@override final bool autoTranslate;
|
||||
@@ -224,6 +227,9 @@ class _AppSettings implements AppSettings {
|
||||
@override final int? appColorScheme;
|
||||
// The color stored via the int type
|
||||
@override final Size? windowSize;
|
||||
// The window size for desktop platforms
|
||||
@override final String? defaultPoolId;
|
||||
@override final String messageDisplayStyle;
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -235,16 +241,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +261,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
|
||||
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
|
||||
});
|
||||
|
||||
|
||||
@@ -272,7 +278,7 @@ class __$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
return _then(_AppSettings(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -284,7 +290,9 @@ as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundI
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,
|
||||
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@ part of 'config.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsNotifierHash() =>
|
||||
r'cd18bff2614a94e3523634e6c577cefad0367eba';
|
||||
r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
|
||||
|
||||
/// See also [AppSettingsNotifier].
|
||||
@ProviderFor(AppSettingsNotifier)
|
||||
|
24
lib/pods/file_pool.dart
Normal file
24
lib/pods/file_pool.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
final poolsProvider = FutureProvider<List<SnFilePool>>((ref) async {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
final response = await dio.get('/drive/pools');
|
||||
return response.data
|
||||
.map((e) => SnFilePool.fromJson(e))
|
||||
.cast<SnFilePool>()
|
||||
.toList();
|
||||
});
|
||||
|
||||
String? resolveDefaultPoolId(WidgetRef ref, List<SnFilePool> pools) {
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
|
||||
final configuredId = settings.defaultPoolId;
|
||||
if (configuredId != null && pools.any((p) => p.id == configuredId)) {
|
||||
return configuredId;
|
||||
}
|
||||
|
||||
return pools.firstOrNull?.id;
|
||||
}
|
@@ -10,17 +10,19 @@ Future<void> resetDatabase(WidgetRef ref) async {
|
||||
if (kIsWeb) return;
|
||||
|
||||
final db = ref.read(databaseProvider);
|
||||
final basepath = await getApplicationSupportDirectory();
|
||||
final file = File(join(basepath.path, 'solar_network_data.sqlite'));
|
||||
|
||||
// Close current database connection
|
||||
db.close();
|
||||
await db.close();
|
||||
|
||||
// Delete database file
|
||||
// Get the correct database file path
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(join(dbFolder.path, 'solar_network_data.sqlite'));
|
||||
|
||||
// Delete database file if it exists
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
// Force refresh the database provider
|
||||
// Force refresh the database provider to create a new instance
|
||||
ref.invalidate(databaseProvider);
|
||||
}
|
||||
|
852
lib/pods/messages_notifier.dart
Normal file
852
lib/pods/messages_notifier.dart
Normal file
@@ -0,0 +1,852 @@
|
||||
import "dart:async";
|
||||
import "dart:developer" as developer;
|
||||
import "package:dio/dio.dart";
|
||||
import "package:drift/drift.dart" show Variable;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/database.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat_rooms.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@riverpod
|
||||
class MessagesNotifier extends _$MessagesNotifier {
|
||||
late final Dio _apiClient;
|
||||
late final AppDatabase _database;
|
||||
late final SnChatRoom _room;
|
||||
late final SnChatMember _identity;
|
||||
|
||||
final Map<String, LocalChatMessage> _pendingMessages = {};
|
||||
final Map<String, Map<int, double>> _fileUploadProgress = {};
|
||||
int? _totalCount;
|
||||
String? _searchQuery;
|
||||
bool? _withLinks;
|
||||
bool? _withAttachments;
|
||||
|
||||
late final String _roomId;
|
||||
static const int _pageSize = 20;
|
||||
bool _hasMore = true;
|
||||
bool _isSyncing = false;
|
||||
bool _isJumping = false;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
_roomId = roomId;
|
||||
_apiClient = ref.watch(apiClientProvider);
|
||||
_database = ref.watch(databaseProvider);
|
||||
final room = await ref.watch(chatroomProvider(roomId).future);
|
||||
final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
|
||||
|
||||
if (room == null) {
|
||||
throw Exception('Room not found');
|
||||
}
|
||||
_room = room;
|
||||
|
||||
// Allow building even if identity is null for public rooms
|
||||
if (identity != null) {
|
||||
_identity = identity;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'MessagesNotifier built for room $roomId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Only setup sync and lifecycle listeners if user is a member
|
||||
if (identity != null) {
|
||||
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
||||
developer.log(
|
||||
'App resumed, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadInitial();
|
||||
return [];
|
||||
}
|
||||
|
||||
List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) {
|
||||
messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Getting cached messages from offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final List<LocalChatMessage> dbMessages;
|
||||
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
|
||||
dbMessages = await _database.searchMessages(
|
||||
_roomId,
|
||||
_searchQuery ?? '',
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
} else {
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
_roomId,
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
}
|
||||
|
||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||
|
||||
if (_withLinks == true) {
|
||||
filteredMessages =
|
||||
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
||||
}
|
||||
|
||||
final dbLocalMessages = filteredMessages;
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in dbLocalMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
final pendingForRoom =
|
||||
_pendingMessages.values
|
||||
.where((msg) => msg.roomId == _roomId)
|
||||
.toList();
|
||||
|
||||
final allMessages = [...pendingForRoom, ...uniqueMessages];
|
||||
_sortMessages(allMessages); // Use the helper function
|
||||
|
||||
final finalUniqueMessages = <LocalChatMessage>[];
|
||||
final finalSeenIds = <String>{};
|
||||
for (final message in allMessages) {
|
||||
if (finalSeenIds.add(message.id)) {
|
||||
finalUniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
return finalUniqueMessages;
|
||||
}
|
||||
|
||||
return uniqueMessages;
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Fetching messages from API, offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
if (_totalCount == null) {
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages',
|
||||
queryParameters: {'offset': 0, 'take': 1},
|
||||
);
|
||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||
}
|
||||
|
||||
if (offset >= _totalCount!) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages',
|
||||
queryParameters: {'offset': offset, 'take': take},
|
||||
);
|
||||
|
||||
final List<dynamic> data = response.data;
|
||||
_totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0');
|
||||
|
||||
final messages =
|
||||
data.map((json) {
|
||||
final remoteMessage = SnChatMessage.fromJson(json);
|
||||
return LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
for (final message in messages) {
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
if (message.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<void> syncMessages() async {
|
||||
if (_isSyncing) {
|
||||
developer.log(
|
||||
'Sync already in progress, skipping.',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_isSyncing = true;
|
||||
|
||||
developer.log('Starting message sync', name: 'MessagesNotifier');
|
||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||
try {
|
||||
final dbMessages = await _database.getMessagesForRoom(
|
||||
_room.id,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
);
|
||||
final lastMessage =
|
||||
dbMessages.isEmpty
|
||||
? null
|
||||
: _database.companionToMessage(dbMessages.first);
|
||||
|
||||
if (lastMessage == null) {
|
||||
developer.log(
|
||||
'No local messages, fetching from network',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final newMessages = await _fetchAndCacheMessages(
|
||||
offset: 0,
|
||||
take: _pageSize,
|
||||
);
|
||||
state = AsyncValue.data(newMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
final resp = await _apiClient.post(
|
||||
'/sphere/chat/${_room.id}/sync',
|
||||
data: {
|
||||
'last_sync_timestamp':
|
||||
lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch,
|
||||
},
|
||||
);
|
||||
|
||||
final response = MessageSyncResponse.fromJson(resp.data);
|
||||
developer.log(
|
||||
'Sync response: ${response.messages.length} changes',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
for (final message in response.messages) {
|
||||
switch (message.type) {
|
||||
case "messages.update":
|
||||
case "messages.update.links":
|
||||
await receiveMessageUpdate(message);
|
||||
break;
|
||||
case "messages.delete":
|
||||
await receiveMessageDeletion(message.id.toString());
|
||||
break;
|
||||
}
|
||||
// Still need receive the message to show the history actions
|
||||
await receiveMessage(message);
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
developer.log('Finished message sync', name: 'MessagesNotifier');
|
||||
Future.microtask(
|
||||
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||
);
|
||||
_isSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> listMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
bool synced = false,
|
||||
}) async {
|
||||
try {
|
||||
if (offset == 0 &&
|
||||
!synced &&
|
||||
(_searchQuery == null || _searchQuery!.isEmpty)) {
|
||||
_fetchAndCacheMessages(offset: 0, take: take).catchError((_) {
|
||||
return <LocalChatMessage>[];
|
||||
});
|
||||
}
|
||||
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
return localMessages;
|
||||
}
|
||||
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
return await _fetchAndCacheMessages(offset: offset, take: take);
|
||||
} else {
|
||||
return []; // If searching, and no local messages, don't fetch from network
|
||||
}
|
||||
} catch (e) {
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
return localMessages;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadInitial() async {
|
||||
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
syncMessages();
|
||||
}
|
||||
|
||||
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
|
||||
|
||||
_hasMore = messages.length == _pageSize;
|
||||
|
||||
state = AsyncValue.data(messages);
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMore || state is AsyncLoading) return;
|
||||
developer.log('Loading more messages', name: 'MessagesNotifier');
|
||||
|
||||
try {
|
||||
final currentMessages = state.value ?? [];
|
||||
final offset = currentMessages.length;
|
||||
|
||||
final newMessages = await listMessages(offset: offset, take: _pageSize);
|
||||
|
||||
if (newMessages.isEmpty || newMessages.length < _pageSize) {
|
||||
_hasMore = false;
|
||||
}
|
||||
|
||||
state = AsyncValue.data(
|
||||
_sortMessages([...currentMessages, ...newMessages]),
|
||||
);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error loading more messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(
|
||||
String content,
|
||||
List<UniversalFile> attachments, {
|
||||
SnChatMessage? editingTo,
|
||||
SnChatMessage? forwardingTo,
|
||||
SnChatMessage? replyingTo,
|
||||
Function(String, Map<int, double>)? onProgress,
|
||||
}) async {
|
||||
final nonce = const Uuid().v4();
|
||||
developer.log(
|
||||
'Sending message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final baseUrl = ref.read(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
|
||||
final mockMessage = SnChatMessage(
|
||||
id: 'pending_$nonce',
|
||||
chatRoomId: _roomId,
|
||||
senderId: _identity.id,
|
||||
content: content,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
nonce: nonce,
|
||||
sender: _identity,
|
||||
);
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
mockMessage,
|
||||
MessageStatus.pending,
|
||||
);
|
||||
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
_fileUploadProgress[localMessage.id] = {};
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
state = AsyncValue.data([localMessage, ...currentMessages]);
|
||||
|
||||
try {
|
||||
var cloudAttachments = List.empty(growable: true);
|
||||
for (var idx = 0; idx < attachments.length; idx++) {
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
fileData: attachments[idx],
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachments[idx].data.name ?? 'Post media',
|
||||
mimetype:
|
||||
attachments[idx].data.mimeType ??
|
||||
switch (attachments[idx].type) {
|
||||
UniversalFileType.image => 'image/unknown',
|
||||
UniversalFileType.video => 'video/unknown',
|
||||
UniversalFileType.audio => 'audio/unknown',
|
||||
UniversalFileType.file => 'application/octet-stream',
|
||||
},
|
||||
onProgress: (progress, _) {
|
||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||
onProgress?.call(
|
||||
localMessage.id,
|
||||
_fileUploadProgress[localMessage.id] ?? {},
|
||||
);
|
||||
},
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
cloudAttachments.add(cloudFile);
|
||||
}
|
||||
|
||||
final response = await _apiClient.request(
|
||||
editingTo == null
|
||||
? '/sphere/chat/$_roomId/messages'
|
||||
: '/sphere/chat/$_roomId/messages/${editingTo.id}',
|
||||
data: {
|
||||
'content': content,
|
||||
'attachments_id': cloudAttachments.map((e) => e.id).toList(),
|
||||
'replied_message_id': replyingTo?.id,
|
||||
'forwarded_message_id': forwardingTo?.id,
|
||||
'meta': {},
|
||||
'nonce': nonce,
|
||||
},
|
||||
options: Options(method: editingTo == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
|
||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
_pendingMessages.remove(localMessage.id);
|
||||
await _database.deleteMessage(localMessage.id);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
if (editingTo != null) {
|
||||
final newMessages =
|
||||
currentMessages
|
||||
.where((m) => m.id != localMessage.id) // remove pending message
|
||||
.map(
|
||||
(m) => m.id == editingTo.id ? updatedMessage : m,
|
||||
) // update original message
|
||||
.toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
} else {
|
||||
final newMessages =
|
||||
currentMessages.map((m) {
|
||||
if (m.id == localMessage.id) {
|
||||
return updatedMessage;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
}
|
||||
developer.log(
|
||||
'Message with nonce $nonce sent successfully',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Failed to send message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
localMessage.status = MessageStatus.failed;
|
||||
_pendingMessages[localMessage.id] = localMessage;
|
||||
await _database.updateMessageStatus(
|
||||
localMessage.id,
|
||||
MessageStatus.failed,
|
||||
);
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == localMessage.id) {
|
||||
return m..status = MessageStatus.failed;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> retryMessage(String pendingMessageId) async {
|
||||
developer.log(
|
||||
'Retrying message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final message = await fetchMessageById(pendingMessageId);
|
||||
if (message == null) {
|
||||
throw Exception('Message not found');
|
||||
}
|
||||
|
||||
message.status = MessageStatus.pending;
|
||||
_pendingMessages[pendingMessageId] = message;
|
||||
await _database.updateMessageStatus(
|
||||
pendingMessageId,
|
||||
MessageStatus.pending,
|
||||
);
|
||||
|
||||
try {
|
||||
var remoteMessage = message.toRemoteMessage();
|
||||
final response = await _apiClient.post(
|
||||
'/sphere/chat/${message.roomId}/messages',
|
||||
data: {
|
||||
'content': remoteMessage.content,
|
||||
'attachments_id': remoteMessage.attachments,
|
||||
'meta': remoteMessage.meta,
|
||||
'nonce': message.nonce,
|
||||
},
|
||||
);
|
||||
|
||||
remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
_pendingMessages.remove(pendingMessageId);
|
||||
await _database.deleteMessage(pendingMessageId);
|
||||
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == pendingMessageId) {
|
||||
return updatedMessage;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
'Failed to retry message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
message.status = MessageStatus.failed;
|
||||
_pendingMessages[pendingMessageId] = message;
|
||||
await _database.updateMessageStatus(
|
||||
pendingMessageId,
|
||||
MessageStatus.failed,
|
||||
);
|
||||
final newMessages =
|
||||
(state.value ?? []).map((m) {
|
||||
if (m.id == pendingMessageId) {
|
||||
return m..status = MessageStatus.failed;
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = AsyncValue.data(_sortMessages(newMessages));
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received new message ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
if (remoteMessage.nonce != null) {
|
||||
_pendingMessages.removeWhere(
|
||||
(_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce,
|
||||
);
|
||||
}
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(localMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final existingIndex = currentMessages.indexWhere(
|
||||
(m) =>
|
||||
m.id == localMessage.id ||
|
||||
(localMessage.nonce != null && m.nonce == localMessage.nonce),
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
final newList = [...currentMessages];
|
||||
newList[existingIndex] = localMessage;
|
||||
state = AsyncValue.data(_sortMessages(newList));
|
||||
} else {
|
||||
state = AsyncValue.data(
|
||||
_sortMessages([localMessage, ...currentMessages]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received message update ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final index = currentMessages.indexWhere((m) => m.id == updatedMessage.id);
|
||||
|
||||
if (index >= 0) {
|
||||
final newList = [...currentMessages];
|
||||
newList[index] = updatedMessage;
|
||||
state = AsyncValue.data(_sortMessages(newList));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageDeletion(String messageId) async {
|
||||
developer.log(
|
||||
'Received message deletion $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
_pendingMessages.remove(messageId);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final messageIndex = currentMessages.indexWhere((m) => m.id == messageId);
|
||||
|
||||
LocalChatMessage? messageToUpdate;
|
||||
if (messageIndex != -1) {
|
||||
messageToUpdate = currentMessages[messageIndex];
|
||||
} else {
|
||||
messageToUpdate = await fetchMessageById(messageId);
|
||||
}
|
||||
|
||||
if (messageToUpdate == null) return;
|
||||
|
||||
final remote = messageToUpdate.toRemoteMessage();
|
||||
final updatedRemote = remote.copyWith(
|
||||
content: 'This message was deleted',
|
||||
deletedAt: DateTime.now(),
|
||||
attachments: [],
|
||||
);
|
||||
|
||||
final deletedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
updatedRemote,
|
||||
messageToUpdate.status,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(deletedMessage));
|
||||
|
||||
if (messageIndex != -1) {
|
||||
final newList = [...currentMessages];
|
||||
newList[messageIndex] = deletedMessage;
|
||||
state = AsyncValue.data(newList);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
developer.log('Deleting message $messageId', name: 'MessagesNotifier');
|
||||
try {
|
||||
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||
await receiveMessageDeletion(messageId);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
'Error deleting message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
||||
_searchQuery = query.trim();
|
||||
_withLinks = withLinks;
|
||||
_withAttachments = withAttachments;
|
||||
loadInitial();
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
_searchQuery = null;
|
||||
_withLinks = null;
|
||||
_withAttachments = null;
|
||||
loadInitial();
|
||||
}
|
||||
|
||||
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
||||
developer.log(
|
||||
'Fetching message by id $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
try {
|
||||
final localMessage =
|
||||
await (_database.select(_database.chatMessages)
|
||||
..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull();
|
||||
if (localMessage != null) {
|
||||
return _database.companionToMessage(localMessage);
|
||||
}
|
||||
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages/$messageId',
|
||||
);
|
||||
final remoteMessage = SnChatMessage.fromJson(response.data);
|
||||
final message = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
MessageStatus.sent,
|
||||
);
|
||||
|
||||
await _database.saveMessage(_database.messageToCompanion(message));
|
||||
return message;
|
||||
} catch (e) {
|
||||
if (e is DioException) return null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> jumpToMessage(String messageId) async {
|
||||
developer.log(
|
||||
'Starting jump to message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
if (_isJumping) {
|
||||
developer.log(
|
||||
'Jump already in progress, skipping',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return -1;
|
||||
}
|
||||
_isJumping = true;
|
||||
|
||||
try {
|
||||
developer.log('Fetching message $messageId', name: 'MessagesNotifier');
|
||||
final message = await fetchMessageById(messageId);
|
||||
if (message == null) {
|
||||
developer.log('Message $messageId not found', name: 'MessagesNotifier');
|
||||
showSnackBar('messageNotFound'.tr());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if message is already in current state to avoid duplicate loading
|
||||
final currentMessages = state.value ?? [];
|
||||
final existingIndex = currentMessages.indexWhere(
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
developer.log(
|
||||
'Message $messageId already in current state at index $existingIndex, jumping directly',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'Message $messageId not in current state, loading messages around it',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Count messages newer than this one
|
||||
final query = _database.customSelect(
|
||||
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
||||
variables: [
|
||||
Variable.withString(_roomId),
|
||||
Variable.withDateTime(message.createdAt),
|
||||
],
|
||||
readsFrom: {_database.chatMessages},
|
||||
);
|
||||
final result = await query.getSingle();
|
||||
final newerCount = result.read<int>('count');
|
||||
|
||||
// Load messages around this position
|
||||
final offset =
|
||||
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||
developer.log(
|
||||
'Loading messages with offset $offset, take $_pageSize',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final loadedMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: _pageSize,
|
||||
);
|
||||
|
||||
// Check if loaded messages are already in current state
|
||||
final currentIds = currentMessages.map((m) => m.id).toSet();
|
||||
final newMessages =
|
||||
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
|
||||
developer.log(
|
||||
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
if (newMessages.isNotEmpty) {
|
||||
// Merge with current messages
|
||||
final allMessages = [...currentMessages, ...newMessages];
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in allMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
_sortMessages(uniqueMessages);
|
||||
state = AsyncValue.data(uniqueMessages);
|
||||
developer.log(
|
||||
'Updated state with ${uniqueMessages.length} total messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
}
|
||||
|
||||
final finalIndex = (state.value ?? []).indexWhere(
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
developer.log(
|
||||
'Final index for message $messageId is $finalIndex',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return finalIndex;
|
||||
} finally {
|
||||
_isJumping = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasLink(LocalChatMessage message) {
|
||||
final content = message.toRemoteMessage().content;
|
||||
if (content == null) return false;
|
||||
final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*');
|
||||
return urlRegex.hasMatch(content);
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'room.dart';
|
||||
part of 'messages_notifier.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4';
|
||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/about.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/screens/developers/app_detail.dart';
|
||||
import 'package:island/screens/developers/bot_detail.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
@@ -19,6 +18,7 @@ import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
import 'package:island/screens/developers/project_detail.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
import 'package:island/screens/files/file_list.dart';
|
||||
import 'package:island/screens/posts/post_categories_list.dart';
|
||||
import 'package:island/screens/posts/post_category_detail.dart';
|
||||
import 'package:island/screens/posts/post_search.dart';
|
||||
@@ -38,7 +38,7 @@ import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/screens/chat/room_detail.dart';
|
||||
import 'package:island/screens/chat/call.dart';
|
||||
import 'package:island/screens/chat/search_messages_screen.dart';
|
||||
import 'package:island/screens/chat/search_messages.dart';
|
||||
import 'package:island/screens/creators/hub.dart';
|
||||
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||
@@ -86,10 +86,7 @@ Widget _tabPagesTransitionBuilder(
|
||||
}
|
||||
|
||||
bool get _supportsAnalytics =>
|
||||
kIsWeb ||
|
||||
Platform.isAndroid ||
|
||||
Platform.isIOS ||
|
||||
Platform.isMacOS;
|
||||
kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
||||
|
||||
// Provider for the router
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
@@ -659,9 +656,9 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'socialCredits',
|
||||
path: '/account/credits',
|
||||
builder: (context, state) => const SocialCreditsScreen(),
|
||||
name: 'files',
|
||||
path: '/account/files',
|
||||
builder: (context, state) => const FileListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'relationships',
|
||||
|
@@ -141,20 +141,22 @@ class AccountScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
GestureDetector(
|
||||
child: LevelingProgressCard(
|
||||
level: user.value!.profile.level,
|
||||
experience: user.value!.profile.experience,
|
||||
progress: user.value!.profile.levelingProgress,
|
||||
),
|
||||
LevelingProgressCard(
|
||||
isCompact: true,
|
||||
level: user.value!.profile.level,
|
||||
experience: user.value!.profile.experience,
|
||||
progress: user.value!.profile.levelingProgress,
|
||||
onTap: () {
|
||||
context.pushNamed('leveling');
|
||||
},
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
@@ -181,6 +183,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
@@ -206,7 +209,73 @@ class AccountScreen extends HookConsumerWidget {
|
||||
).height(140),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.settings, size: 28).padding(bottom: 8),
|
||||
Text('appSettings').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.person_edit,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('updateYourProfile').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('profileUpdate');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.manage_accounts,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('accountSettings').tr().fontSize(16).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.notifications),
|
||||
@@ -235,6 +304,16 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('wallet');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.files),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('files').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('files');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.people),
|
||||
@@ -265,16 +344,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('webFeedMarketplace');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.star),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('credits').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('socialCredits');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('abuseReport').tr(),
|
||||
@@ -284,37 +353,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
onTap: () => context.pushNamed('reportList'),
|
||||
),
|
||||
const Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.settings),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('appSettings').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.person_edit),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('updateYourProfile').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('profileUpdate');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.manage_accounts),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('accountSettings').tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
const Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.info),
|
||||
@@ -333,6 +371,8 @@ class AccountScreen extends HookConsumerWidget {
|
||||
title: Text('debugOptions').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => DebugSheet(),
|
||||
);
|
||||
|
@@ -4,7 +4,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -59,94 +58,93 @@ class SocialCreditHistoryNotifier extends _$SocialCreditHistoryNotifier
|
||||
}
|
||||
}
|
||||
|
||||
class SocialCreditsScreen extends HookConsumerWidget {
|
||||
const SocialCreditsScreen({super.key});
|
||||
class SocialCreditsTab extends HookConsumerWidget {
|
||||
const SocialCreditsTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final socialCredits = ref.watch(socialCreditsProvider);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('socialCredits').tr()),
|
||||
body: Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: socialCredits
|
||||
.when(
|
||||
data:
|
||||
(credits) => Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text(
|
||||
'${credits.toStringAsFixed(2)} pts',
|
||||
).fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
return Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: socialCredits
|
||||
.when(
|
||||
data:
|
||||
(credits) => Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text(
|
||||
'${credits.toStringAsFixed(2)} pts',
|
||||
).fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (_, _) => Text('Error loading credits'),
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
)
|
||||
.padding(horizontal: 20, vertical: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: PagingHelperView(
|
||||
provider: socialCreditHistoryNotifierProvider,
|
||||
futureRefreshable: socialCreditHistoryNotifierProvider.future,
|
||||
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
final record = data.items[index];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
error: (_, _) => Text('Error loading credits'),
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
)
|
||||
.padding(horizontal: 20, vertical: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: PagingHelperView(
|
||||
provider: socialCreditHistoryNotifierProvider,
|
||||
futureRefreshable: socialCreditHistoryNotifierProvider.future,
|
||||
notifierRefreshable: socialCreditHistoryNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
final record = data.items[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(record.reason),
|
||||
subtitle: Text(
|
||||
DateFormat.yMMMd().format(record.createdAt),
|
||||
title: Text(record.reason),
|
||||
subtitle: Text(
|
||||
DateFormat.yMMMd().format(record.createdAt),
|
||||
),
|
||||
trailing: Text(
|
||||
record.delta > 0
|
||||
? '+${record.delta}'
|
||||
: '${record.delta}',
|
||||
style: TextStyle(
|
||||
color: record.delta > 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
trailing: Text(
|
||||
record.delta > 0
|
||||
? '+${record.delta}'
|
||||
: '${record.delta}',
|
||||
style: TextStyle(
|
||||
color: record.delta > 0 ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,12 +4,12 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/wallet.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/account/leveling_progress.dart';
|
||||
@@ -89,7 +89,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
length: 3,
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('levelingProgress'.tr()),
|
||||
@@ -104,6 +104,15 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'socialCredits'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'stellarProgram'.tr(),
|
||||
@@ -119,6 +128,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_buildLevelingTab(context, ref, user.value!),
|
||||
const SocialCreditsTab(),
|
||||
_buildStellarProgramTab(context, ref),
|
||||
],
|
||||
),
|
||||
@@ -138,7 +148,6 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(20),
|
||||
@@ -164,10 +173,33 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
),
|
||||
const SliverGap(16),
|
||||
|
||||
// Stairs visualization with fixed height and horizontal scroll
|
||||
SliverToBoxAdapter(child: _buildLevelStairs(context, currentLevel)),
|
||||
const SliverGap(24),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120',
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(
|
||||
value: currentLevel / 120,
|
||||
minHeight: 10,
|
||||
stopIndicatorRadius: 0,
|
||||
trackGap: 0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, top: 16, bottom: 12),
|
||||
),
|
||||
),
|
||||
const SliverGap(16),
|
||||
// Leveling History
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
@@ -239,137 +271,12 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLevelStairs(BuildContext context, int currentLevel) {
|
||||
const totalLevels = 14;
|
||||
const stairHeight = 20.0;
|
||||
const stairWidth = 50.0;
|
||||
const containerHeight = 280.0;
|
||||
|
||||
return Container(
|
||||
height: containerHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SizedBox(
|
||||
width: (totalLevels * (stairWidth + 8)) + 40,
|
||||
height: containerHeight,
|
||||
child: CustomPaint(
|
||||
painter: LevelStairsPainter(
|
||||
currentLevel: currentLevel,
|
||||
totalLevels: totalLevels,
|
||||
primaryColor: Theme.of(context).colorScheme.primary,
|
||||
surfaceColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
onSurfaceColor: Theme.of(context).colorScheme.onSurface,
|
||||
stairHeight: stairHeight,
|
||||
stairWidth: stairWidth,
|
||||
),
|
||||
child: Stack(
|
||||
children: List.generate(totalLevels, (index) {
|
||||
final level = index + 1;
|
||||
final isCompleted = level <= currentLevel;
|
||||
final isCurrent = level == currentLevel;
|
||||
|
||||
// Calculate position from bottom
|
||||
final bottomPosition = 0.0;
|
||||
final leftPosition = 20.0 + (index * (stairWidth + 8));
|
||||
|
||||
// Make higher levels progressively taller
|
||||
final progressiveHeight =
|
||||
40.0 + (index * 15.0); // Base height + progressive increase
|
||||
|
||||
return Positioned(
|
||||
left: leftPosition,
|
||||
bottom: bottomPosition,
|
||||
child: Container(
|
||||
width: stairWidth,
|
||||
height: progressiveHeight,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCompleted
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
),
|
||||
border:
|
||||
isCurrent
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
boxShadow:
|
||||
isCurrent
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
level.toString(),
|
||||
style: GoogleFonts.robotoMono(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
isCompleted
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isCurrent) ...[
|
||||
const Gap(4),
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@@ -66,7 +66,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
|
||||
}
|
||||
|
||||
String _$accountAppbarForcegroundColorHash() =>
|
||||
r'8ee0cae10817b77fb09548a482f5247662b4374c';
|
||||
r'127fcc7fd6ec6a41ac4a6975276b5271aa4fa7d0';
|
||||
|
||||
/// See also [accountAppbarForcegroundColor].
|
||||
@ProviderFor(accountAppbarForcegroundColor)
|
||||
|
@@ -1,17 +1,10 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'captcha.config.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<String> captchaUrl(Ref ref) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/.well-known/services');
|
||||
final serviceMapping = await resp.data;
|
||||
var baseUrl = serviceMapping['DysonNetwork.Pass'] as String;
|
||||
// The backend using self-signed certicates on development
|
||||
// Which mobile simulator might not accept, use this to avoid errors
|
||||
if (baseUrl.contains('https://localhost')) baseUrl = 'http://localhost:5216';
|
||||
return '$baseUrl/captcha';
|
||||
const baseUrl = "https://solian.app";
|
||||
return '$baseUrl/auth/captcha';
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ part of 'captcha.config.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$captchaUrlHash() => r'bbed0d18272dd205069642b3c6583ea2eef735d1';
|
||||
String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8';
|
||||
|
||||
/// See also [captchaUrl].
|
||||
@ProviderFor(captchaUrl)
|
||||
|
@@ -543,7 +543,7 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
221
lib/screens/chat/public_room_preview.dart
Normal file
221
lib/screens/chat/public_room_preview.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/widgets/content/cloud_files.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:go_router/go_router.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:island/widgets/app_scaffold.dart";
|
||||
import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
final SnChatRoom room;
|
||||
|
||||
const PublicRoomPreview({super.key, required this.id, required this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final messages = ref.watch(messagesNotifierProvider(id));
|
||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
var isLoading = false;
|
||||
|
||||
// Add scroll listener for pagination
|
||||
useEffect(() {
|
||||
void onScroll() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
final valueKey = key as ValueKey;
|
||||
final messageId = valueKey.value as String;
|
||||
return messageList.indexWhere((m) => m.id == messageId);
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
return MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false, // User is not a member, so not current user
|
||||
onAction: null, // No actions allowed in preview mode
|
||||
onJump: (_) {}, // No jump functionality in preview
|
||||
progress: null,
|
||||
showAvatar: isLastInGroup,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final compactHeader = isWideScreen(context);
|
||||
|
||||
Widget comfortHeaderWidget() => Column(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(15),
|
||||
],
|
||||
);
|
||||
|
||||
Widget compactHeaderWidget() => Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(19),
|
||||
],
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: compactHeader ? null : 64,
|
||||
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
context.pushNamed('chatDetail', pathParameters: {'id': id});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: messages.when(
|
||||
data:
|
||||
(messageList) =>
|
||||
messageList.isEmpty
|
||||
? Center(child: Text('No messages yet'.tr()))
|
||||
: chatMessageListWidget(messageList),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => messagesNotifier.loadInitial(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Join button at the bottom for public rooms
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post('/sphere/chat/${room.id}/members/me');
|
||||
ref.invalidate(chatroomIdentityProvider(id));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
label: Text('chatJoin').tr(),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/pods/database.dart';
|
||||
import 'package:island/screens/chat/search_messages.dart';
|
||||
|
||||
part 'room_detail.freezed.dart';
|
||||
part 'room_detail.g.dart';
|
||||
@@ -401,11 +402,17 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
onTap: () async {
|
||||
final result = await context.pushNamed(
|
||||
'searchMessages',
|
||||
pathParameters: {'id': id},
|
||||
);
|
||||
if (result is SearchMessagesResult) {
|
||||
// Navigate back to room screen with message to jump to
|
||||
if (context.mounted) {
|
||||
context.pop(result.messageId);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@@ -1,13 +1,20 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_item.dart';
|
||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
// Class to represent the result when popping from search messages
|
||||
class SearchMessagesResult {
|
||||
final String messageId;
|
||||
const SearchMessagesResult(this.messageId);
|
||||
}
|
||||
|
||||
class SearchMessagesScreen extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
|
||||
@@ -112,24 +119,24 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
||||
? Center(child: Text('noMessagesFound'.tr()))
|
||||
: SuperListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
reverse: false, // Show newest messages at the top
|
||||
itemCount: messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
// Simplified MessageItem for search results, no grouping logic
|
||||
return MessageItem(
|
||||
return MessageListTile(
|
||||
message: message,
|
||||
isCurrentUser:
|
||||
false, // Or determine based on actual user
|
||||
onAction: null,
|
||||
onJump: (_) {},
|
||||
progress: null,
|
||||
showAvatar: true,
|
||||
onJump: (messageId) {
|
||||
// Return the search result and pop back to room detail
|
||||
context.pop(SearchMessagesResult(messageId));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))),
|
||||
error:
|
||||
(error, _) => Center(
|
||||
child: Text('errorGeneric'.tr(args: [error.toString()])),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
@@ -78,6 +78,7 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
replacePath: true,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
CropAspectRatio(height: 7, width: 16)
|
||||
@@ -98,7 +99,7 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -141,7 +141,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -127,7 +127,7 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
556
lib/screens/files/file_list.dart
Normal file
556
lib/screens/files/file_list.dart
Normal file
@@ -0,0 +1,556 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'file_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<SnCloudFile> {
|
||||
String? _poolId;
|
||||
bool _includeRecycled = false;
|
||||
|
||||
void setFilters(String? poolId, bool includeRecycled) {
|
||||
_poolId = poolId;
|
||||
_includeRecycled = includeRecycled;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final offset = cursor == null ? 0 : int.parse(cursor);
|
||||
final take = 20;
|
||||
|
||||
final queryParameters = <String, dynamic>{'offset': offset, 'take': take};
|
||||
|
||||
// Add filter parameters
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
if (_includeRecycled) {
|
||||
queryParameters['recycled'] = 'true';
|
||||
}
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/files/me',
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<SnCloudFile> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final hasMore = offset + items.length < total;
|
||||
final nextCursor = hasMore ? (offset + items.length).toString() : null;
|
||||
|
||||
return CursorPagingData(
|
||||
items: items,
|
||||
hasMore: hasMore,
|
||||
nextCursor: nextCursor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/usage');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get('/drive/billing/quota');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
class FileListScreen extends HookConsumerWidget {
|
||||
const FileListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Filter state
|
||||
final selectedPool = useState<String?>(null);
|
||||
final includeRecycled = useState(false);
|
||||
|
||||
final usageAsync = ref.watch(billingUsageProvider);
|
||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||
|
||||
// Update notifier filters when state changes
|
||||
useEffect(() {
|
||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
notifier.setFilters(selectedPool.value, includeRecycled.value);
|
||||
return null;
|
||||
}, [selectedPool.value, includeRecycled.value]);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text('Files'), leading: const PageBackButton()),
|
||||
body: usageAsync.when(
|
||||
data:
|
||||
(usage) => quotaAsync.when(
|
||||
data:
|
||||
(quota) => _buildQuotaUI(
|
||||
usage,
|
||||
quota,
|
||||
ref,
|
||||
selectedPool,
|
||||
includeRecycled,
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error loading usage')),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuotaUI(
|
||||
Map<String, dynamic>? usage,
|
||||
Map<String, dynamic>? quota,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
if (usage == null) return const SizedBox.shrink();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Uploads',
|
||||
'${((usage['total_usage_bytes'] as num) / (1024 * 1024 * 1024)).toStringAsFixed(3)} GiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'All Files',
|
||||
'${usage['total_file_count']}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Quota',
|
||||
'${usage['total_quota']} MiB',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
'Used Quota',
|
||||
'${((usage['used_quota'] as num) / (usage['total_quota'] as num) * 100).toStringAsFixed(2)}%',
|
||||
progress:
|
||||
(usage['used_quota'] as num) /
|
||||
(usage['total_quota'] as num),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Pool Usage'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildPoolChartData(usage)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('Verbose Quota'),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(_buildQuotaChartData(quota)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildFilters(ref, selectedPool, includeRecycled),
|
||||
),
|
||||
const SliverGap(8),
|
||||
PagingHelperSliverView(
|
||||
provider: cloudFileListNotifierProvider,
|
||||
futureRefreshable: cloudFileListNotifierProvider.future,
|
||||
notifierRefreshable: cloudFileListNotifierProvider.notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
final itemType = item.mimeType?.split('/').firstOrNull;
|
||||
return ListTile(
|
||||
leading: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: switch (itemType) {
|
||||
'image' => CloudImageWidget(file: item),
|
||||
'audio' =>
|
||||
const Icon(Symbols.audio_file, fill: 1).center(),
|
||||
'video' =>
|
||||
const Icon(Symbols.video_file, fill: 1).center(),
|
||||
_ =>
|
||||
const Icon(Symbols.body_system, fill: 1).center(),
|
||||
},
|
||||
),
|
||||
),
|
||||
title:
|
||||
item.name.isEmpty
|
||||
? Text('untitled').tr().italic()
|
||||
: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(formatFileSize(item.size)),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
},
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteFile'.tr(),
|
||||
'deleteFile'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/${item.id}');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteFile'.tr());
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildPoolChartData(Map<String, dynamic> usage) {
|
||||
final pools = usage['pool_usages'] as List<dynamic>;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.red,
|
||||
Colors.purple,
|
||||
];
|
||||
return PieChartData(
|
||||
sections:
|
||||
pools.asMap().entries.map((entry) {
|
||||
final pool = entry.value as Map<String, dynamic>;
|
||||
final title = pool['pool_name'] as String;
|
||||
final truncatedTitle =
|
||||
title.length > 8 ? '${title.substring(0, 8)}...' : title;
|
||||
return PieChartSectionData(
|
||||
value: (pool['usage_bytes'] as num).toDouble(),
|
||||
title: truncatedTitle,
|
||||
color: colors[entry.key % colors.length],
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
PieChartData _buildQuotaChartData(Map<String, dynamic>? quota) {
|
||||
if (quota == null) return PieChartData(sections: []);
|
||||
return PieChartData(
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: (quota['based_quota'] as num).toDouble(),
|
||||
title: 'Base',
|
||||
color: Colors.green,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: (quota['extra_quota'] as num).toDouble(),
|
||||
title: 'Extra',
|
||||
color: Colors.orange,
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedPool,
|
||||
ValueNotifier<bool> includeRecycled,
|
||||
) {
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'filters'.tr(),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Gap(16),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 600;
|
||||
return isWide
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
poolsAsync.when(
|
||||
data:
|
||||
(pools) => DropdownButtonFormField<String?>(
|
||||
value: selectedPool.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pool',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('allPools'.tr()),
|
||||
),
|
||||
...pools.map(
|
||||
(pool) => DropdownMenuItem<String?>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
(value) => selectedPool.value = value,
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (e, _) => const Text('Error loading pools'),
|
||||
),
|
||||
const Gap(16),
|
||||
Row(
|
||||
children: [
|
||||
Text('includeRecycled'.tr()),
|
||||
const Gap(8),
|
||||
Switch(
|
||||
value: includeRecycled.value,
|
||||
onChanged:
|
||||
(value) => includeRecycled.value = value,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
tooltip: 'deleteRecycledFiles'.tr(),
|
||||
onPressed:
|
||||
includeRecycled.value
|
||||
? () => _deleteRecycledFiles(ref)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
).padding(horizontal: 8);
|
||||
}
|
||||
|
||||
Future<void> _deleteRecycledFiles(WidgetRef ref) async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'confirmDeleteRecycledFiles'.tr(),
|
||||
'deleteRecycledFiles'.tr(),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
if (ref.context.mounted) showLoadingModal(ref.context);
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/drive/files/recycled');
|
||||
ref.invalidate(cloudFileListNotifierProvider);
|
||||
showSnackBar('recycledFilesDeleted'.tr());
|
||||
} catch (e) {
|
||||
showSnackBar('failedToDeleteRecycledFiles'.tr());
|
||||
} finally {
|
||||
if (ref.context.mounted) hideLoadingModal(ref.context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String label, String value, {double? progress}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontSize: 14)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (progress != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(value: progress),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
69
lib/screens/files/file_list.g.dart
Normal file
69
lib/screens/files/file_list.g.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$billingUsageHash() => r'58d8bc774868d60781574c85d6b25869a79c57aa';
|
||||
|
||||
/// See also [billingUsage].
|
||||
@ProviderFor(billingUsage)
|
||||
final billingUsageProvider =
|
||||
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
|
||||
billingUsage,
|
||||
name: r'billingUsageProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$billingUsageHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef BillingUsageRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$billingQuotaHash() => r'4ec5d728e439015800abb2d0d673b5a7329cc654';
|
||||
|
||||
/// See also [billingQuota].
|
||||
@ProviderFor(billingQuota)
|
||||
final billingQuotaProvider =
|
||||
AutoDisposeFutureProvider<Map<String, dynamic>?>.internal(
|
||||
billingQuota,
|
||||
name: r'billingQuotaProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$billingQuotaHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||
String _$cloudFileListNotifierHash() =>
|
||||
r'22c45a8ea23147a3835ba870ad2f0bb833f853ea';
|
||||
|
||||
/// See also [CloudFileListNotifier].
|
||||
@ProviderFor(CloudFileListNotifier)
|
||||
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||
CloudFileListNotifier,
|
||||
CursorPagingData<SnCloudFile>
|
||||
>.internal(
|
||||
CloudFileListNotifier.new,
|
||||
name: r'cloudFileListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$cloudFileListNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$CloudFileListNotifier =
|
||||
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@@ -7,7 +7,7 @@ part of 'notification.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$notificationUnreadCountNotifierHash() =>
|
||||
r'0763b66bd64e5a9b7c317887e109ab367515dfa4';
|
||||
r'08c773809958d96a7ce82acf04af1f9e0b23e119';
|
||||
|
||||
/// See also [NotificationUnreadCountNotifier].
|
||||
@ProviderFor(NotificationUnreadCountNotifier)
|
||||
@@ -28,7 +28,7 @@ final notificationUnreadCountNotifierProvider =
|
||||
|
||||
typedef _$NotificationUnreadCountNotifier = AutoDisposeAsyncNotifier<int>;
|
||||
String _$notificationListNotifierHash() =>
|
||||
r'5099466db475bbcf1ab6b514eb072f1dc4c6f930';
|
||||
r'260046e11f45b0d67ab25bcbdc8604890d71ccc7';
|
||||
|
||||
/// See also [NotificationListNotifier].
|
||||
@ProviderFor(NotificationListNotifier)
|
||||
|
@@ -10,6 +10,7 @@ import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/posts/compose_article.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
@@ -225,8 +226,26 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
||||
onRequestUpload: () async {
|
||||
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
||||
@@ -253,8 +272,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(ref, state, idx),
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete:
|
||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
|
@@ -11,6 +11,7 @@ import 'package:island/models/post.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -345,12 +346,30 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
isCompact: true,
|
||||
item: attachments[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload:
|
||||
() => ComposeLogic.uploadAttachment(
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<
|
||||
AttachmentUploadConfig
|
||||
>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
),
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onUpdate:
|
||||
(value) =>
|
||||
ComposeLogic.updateAttachment(
|
||||
|
@@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement
|
||||
}
|
||||
|
||||
String _$publisherAppbarForcegroundColorHash() =>
|
||||
r'd781a806a242aea5c1609ec98c97c52fdd9f7db1';
|
||||
r'cd9a9816177a6eecc2bc354acebbbd48892ffdd7';
|
||||
|
||||
/// See also [publisherAppbarForcegroundColor].
|
||||
@ProviderFor(publisherAppbarForcegroundColor)
|
||||
|
@@ -7,7 +7,7 @@ part of 'realm_detail.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$realmAppbarForegroundColorHash() =>
|
||||
r'14b5563d861996ea182d0d2db7aa5c2bb3bbaf48';
|
||||
r'8131c047a984318a4cc3fbb5daa5ef0ce44dfae5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -211,7 +211,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
|
@@ -12,6 +12,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/color_extraction.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
@@ -20,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
|
||||
class SettingsScreen extends HookConsumerWidget {
|
||||
const SettingsScreen({super.key});
|
||||
@@ -33,7 +35,8 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
final isDesktop =
|
||||
!kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux);
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
final pools = ref.watch(poolsProvider);
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final docBasepath = useState<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
@@ -127,6 +130,48 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Message display style settings
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsMessageDisplayStyle').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.chat),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
isExpanded: true,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'bubble',
|
||||
child: Text('Bubble').fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'column',
|
||||
child: Text('Column').fontSize(14),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'compact',
|
||||
child: Text('Compact').fontSize(14),
|
||||
),
|
||||
],
|
||||
value: settings.messageDisplayStyle,
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setMessageDisplayStyle(value);
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Color scheme settings
|
||||
ListTile(
|
||||
minLeadingWidth: 48,
|
||||
@@ -367,6 +412,71 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (user.value != null)
|
||||
pools.when(
|
||||
data: (data) {
|
||||
final validPools = data;
|
||||
final currentPoolId = resolveDefaultPoolId(ref, data);
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsDefaultPool').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
leading: const Icon(Symbols.cloud),
|
||||
subtitle: Text(
|
||||
'settingsDefaultPoolHelper'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<String>(
|
||||
isExpanded: true,
|
||||
items:
|
||||
validPools.map((p) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: p.id,
|
||||
child: Tooltip(
|
||||
message: p.name,
|
||||
child: Text(
|
||||
p.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).fontSize(14),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: currentPoolId,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setDefaultPoolId(value);
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
height: 40,
|
||||
width: 120,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(height: 40),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => const ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('Loading pools...'),
|
||||
leading: CircularProgressIndicator(),
|
||||
),
|
||||
error:
|
||||
(err, st) => ListTile(
|
||||
minLeadingWidth: 48,
|
||||
title: Text('settingsDefaultPool').tr(),
|
||||
subtitle: Text('Error: $err'),
|
||||
leading: const Icon(Icons.error, color: Colors.red),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final behaviorSettings = [
|
||||
|
@@ -11,11 +11,13 @@ import 'package:island/services/file_uploader.dart';
|
||||
import 'package:native_exif/native_exif.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
enum FileUploadMode { generic, mediaSafe }
|
||||
|
||||
Future<XFile?> cropImage(
|
||||
BuildContext context, {
|
||||
required XFile image,
|
||||
List<CropAspectRatio?>? allowedAspectRatios,
|
||||
bool replacePath = false,
|
||||
bool replacePath = true,
|
||||
}) async {
|
||||
final result = await showMaterialImageCropper(
|
||||
context,
|
||||
@@ -40,49 +42,46 @@ Future<XFile?> cropImage(
|
||||
);
|
||||
}
|
||||
|
||||
Completer<SnCloudFile?> putMediaToCloud({
|
||||
Completer<SnCloudFile?> putFileToCloud({
|
||||
required UniversalFile fileData,
|
||||
required String atk,
|
||||
required String baseUrl,
|
||||
String? poolId,
|
||||
String? filename,
|
||||
String? mimetype,
|
||||
FileUploadMode? mode,
|
||||
Function(double progress, Duration estimate)? onProgress,
|
||||
}) {
|
||||
final completer = Completer<SnCloudFile?>();
|
||||
|
||||
// Process the image to remove GPS EXIF data if needed
|
||||
if (fileData.isOnDevice && fileData.type == UniversalFileType.image) {
|
||||
final effectiveMode =
|
||||
mode ??
|
||||
(fileData.type == UniversalFileType.file
|
||||
? FileUploadMode.generic
|
||||
: FileUploadMode.mediaSafe);
|
||||
|
||||
if (effectiveMode == FileUploadMode.mediaSafe &&
|
||||
fileData.isOnDevice &&
|
||||
fileData.type == UniversalFileType.image) {
|
||||
final data = fileData.data;
|
||||
if (data is XFile && !kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
// Use native_exif to selectively remove GPS data
|
||||
Exif.fromPath(data.path)
|
||||
.then((exif) {
|
||||
// Remove GPS-related attributes
|
||||
final gpsAttributes = [
|
||||
'GPSLatitude',
|
||||
'GPSLatitudeRef',
|
||||
'GPSLongitude',
|
||||
'GPSLongitudeRef',
|
||||
'GPSAltitude',
|
||||
'GPSAltitudeRef',
|
||||
'GPSTimeStamp',
|
||||
'GPSProcessingMethod',
|
||||
'GPSDateStamp',
|
||||
];
|
||||
|
||||
// Create a map of attributes to clear
|
||||
final clearAttributes = <String, String>{};
|
||||
for (final attr in gpsAttributes) {
|
||||
clearAttributes[attr] = '';
|
||||
}
|
||||
|
||||
// Write empty values to remove GPS data
|
||||
return exif.writeAttributes(clearAttributes);
|
||||
.then((exif) async {
|
||||
final gpsAttributes = {
|
||||
'GPSLatitude': '',
|
||||
'GPSLatitudeRef': '',
|
||||
'GPSLongitude': '',
|
||||
'GPSLongitudeRef': '',
|
||||
'GPSAltitude': '',
|
||||
'GPSAltitudeRef': '',
|
||||
'GPSTimeStamp': '',
|
||||
'GPSProcessingMethod': '',
|
||||
'GPSDateStamp': '',
|
||||
};
|
||||
await exif.writeAttributes(gpsAttributes);
|
||||
})
|
||||
.then((_) {
|
||||
// Continue with upload after GPS data is removed
|
||||
_processUpload(
|
||||
.then(
|
||||
(_) => _processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
baseUrl,
|
||||
@@ -91,12 +90,11 @@ Completer<SnCloudFile?> putMediaToCloud({
|
||||
mimetype,
|
||||
onProgress,
|
||||
completer,
|
||||
);
|
||||
})
|
||||
),
|
||||
)
|
||||
.catchError((e) {
|
||||
// If there's an error, continue with the original file
|
||||
debugPrint('Error removing GPS EXIF data: $e');
|
||||
_processUpload(
|
||||
return _processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
baseUrl,
|
||||
@@ -112,7 +110,6 @@ Completer<SnCloudFile?> putMediaToCloud({
|
||||
}
|
||||
}
|
||||
|
||||
// If not an image or on web, continue with normal upload
|
||||
_processUpload(
|
||||
fileData,
|
||||
atk,
|
||||
@@ -214,8 +211,9 @@ Completer<SnCloudFile?> _processUpload(
|
||||
getTemporaryDirectory()
|
||||
.then((tempDir) {
|
||||
final tempFile = File('${tempDir.path}/temp_upload_$actualFilename');
|
||||
tempFile
|
||||
.writeAsBytes(byteData!)
|
||||
file
|
||||
.readAsBytes()
|
||||
.then((bytes) => tempFile.writeAsBytes(bytes))
|
||||
.then((_) {
|
||||
fileObj = tempFile;
|
||||
// Call progress start
|
||||
|
122
lib/utils/activity_utils.dart
Normal file
122
lib/utils/activity_utils.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
|
||||
String? getActivityTitle(String? label, Map<String, dynamic>? meta) {
|
||||
if (meta == null) return label;
|
||||
if (meta['assets']?['large_text'] is String) {
|
||||
return meta['assets']?['large_text'];
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
String? getActivitySubtitle(Map<String, dynamic>? meta) {
|
||||
if (meta == null) return null;
|
||||
if (meta['assets']?['small_text'] is String) {
|
||||
return meta['assets']?['small_text'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
InlineSpan getActivityFullMessage(SnAccountStatus? status) {
|
||||
if (status?.meta == null) return TextSpan(text: 'No activity details available');
|
||||
final meta = status!.meta!;
|
||||
final List<InlineSpan> spans = [];
|
||||
if (meta.containsKey('assets') && meta['assets'] is Map) {
|
||||
final assets = meta['assets'] as Map<String, dynamic>;
|
||||
if (assets.containsKey('large_text')) {
|
||||
spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold)));
|
||||
}
|
||||
if (assets.containsKey('small_text')) {
|
||||
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
|
||||
spans.add(TextSpan(text: assets['small_text']));
|
||||
}
|
||||
}
|
||||
String normalText = '';
|
||||
if (meta.containsKey('details')) {
|
||||
normalText += 'Details: ${meta['details']}\n';
|
||||
}
|
||||
if (meta.containsKey('state')) {
|
||||
normalText += 'State: ${meta['state']}\n';
|
||||
}
|
||||
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
|
||||
final ts = meta['timestamps'] as Map<String, dynamic>;
|
||||
if (ts.containsKey('start') && ts['start'] is int) {
|
||||
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
|
||||
normalText += 'Started: ${start.toLocal()}\n';
|
||||
}
|
||||
if (ts.containsKey('end') && ts['end'] is int) {
|
||||
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
|
||||
normalText += 'Ends: ${end.toLocal()}\n';
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('party') && meta['party'] is Map) {
|
||||
final party = meta['party'] as Map<String, dynamic>;
|
||||
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
|
||||
final size = party['size'] as List;
|
||||
normalText += 'Party: ${size[0]}/${size[1]}\n';
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('instance')) {
|
||||
normalText += 'Instance: ${meta['instance']}\n';
|
||||
}
|
||||
// Add other keys if present
|
||||
meta.forEach((key, value) {
|
||||
if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) {
|
||||
normalText += '$key: $value\n';
|
||||
}
|
||||
});
|
||||
if (normalText.isNotEmpty) {
|
||||
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
|
||||
spans.add(TextSpan(text: normalText.trimRight()));
|
||||
}
|
||||
return TextSpan(children: spans);
|
||||
}
|
||||
|
||||
Widget buildActivityDetails(SnAccountStatus? status) {
|
||||
if (status?.meta == null) return Text('No activity details available');
|
||||
final meta = status!.meta!;
|
||||
final List<Widget> children = [];
|
||||
if (meta.containsKey('assets') && meta['assets'] is Map) {
|
||||
final assets = meta['assets'] as Map<String, dynamic>;
|
||||
if (assets.containsKey('large_text')) {
|
||||
children.add(Text(assets['large_text']));
|
||||
}
|
||||
if (assets.containsKey('small_text')) {
|
||||
children.add(Text(assets['small_text']));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('details')) {
|
||||
children.add(Text('Details: ${meta['details']}'));
|
||||
}
|
||||
if (meta.containsKey('state')) {
|
||||
children.add(Text('State: ${meta['state']}'));
|
||||
}
|
||||
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
|
||||
final ts = meta['timestamps'] as Map<String, dynamic>;
|
||||
if (ts.containsKey('start') && ts['start'] is int) {
|
||||
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
|
||||
children.add(Text('Started: ${start.toLocal()}'));
|
||||
}
|
||||
if (ts.containsKey('end') && ts['end'] is int) {
|
||||
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
|
||||
children.add(Text('Ends: ${end.toLocal()}'));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('party') && meta['party'] is Map) {
|
||||
final party = meta['party'] as Map<String, dynamic>;
|
||||
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
|
||||
final size = party['size'] as List;
|
||||
children.add(Text('Party: ${size[0]}/${size[1]}'));
|
||||
}
|
||||
}
|
||||
if (meta.containsKey('instance')) {
|
||||
children.add(Text('Instance: ${meta['instance']}'));
|
||||
}
|
||||
// Add other keys if present
|
||||
children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}')));
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
}
|
@@ -290,8 +290,9 @@ class AccountSessionSheet extends HookConsumerWidget {
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted)
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return confirm;
|
||||
|
@@ -147,6 +147,7 @@ class AccountProfileCard extends HookConsumerWidget {
|
||||
if (data.badges.isNotEmpty)
|
||||
BadgeList(badges: data.badges).padding(top: 12),
|
||||
LevelingProgressCard(
|
||||
isCompact: true,
|
||||
level: data.profile.level,
|
||||
experience: data.profile.experience,
|
||||
progress: data.profile.levelingProgress,
|
||||
|
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/activity.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/activity_utils.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -75,7 +76,10 @@ class EventDetailsWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(status.label),
|
||||
if ((getActivityTitle(status.label, status.meta) ?? status.label).isNotEmpty)
|
||||
Text(getActivityTitle(status.label, status.meta) ?? status.label),
|
||||
if (getActivitySubtitle(status.meta) != null)
|
||||
Text(getActivitySubtitle(status.meta)!).fontSize(11).opacity(0.8),
|
||||
Text(
|
||||
'${status.createdAt.formatSystem()} - ${status.clearedAt?.formatSystem() ?? 'present'.tr()}',
|
||||
).fontSize(11).opacity(0.8),
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -8,50 +7,152 @@ class LevelingProgressCard extends StatelessWidget {
|
||||
final int level;
|
||||
final int experience;
|
||||
final double progress;
|
||||
final VoidCallback? onTap;
|
||||
final bool isCompact;
|
||||
|
||||
const LevelingProgressCard({
|
||||
super.key,
|
||||
required this.level,
|
||||
required this.experience,
|
||||
required this.progress,
|
||||
this.onTap,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
// Calculate level stage (1-12, each stage covers 10 levels)
|
||||
int stage = ((level - 1) ~/ 10) + 1;
|
||||
stage = stage.clamp(1, 12); // Ensure stage is within 1-12
|
||||
|
||||
// Define colors for each stage
|
||||
const List<Color> stageColors = [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.teal,
|
||||
Colors.cyan,
|
||||
Colors.indigo,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.pink,
|
||||
Colors.red,
|
||||
];
|
||||
|
||||
Color stageColor = stageColors[stage - 1];
|
||||
|
||||
// Compact mode adjustments
|
||||
final double levelFontSize = isCompact ? 14 : 18;
|
||||
final double stageFontSize = isCompact ? 13 : 14;
|
||||
final double experienceFontSize = isCompact ? 12 : 14;
|
||||
final double progressHeight = isCompact ? 6 : 10;
|
||||
final double horizontalPadding = isCompact ? 16 : 20;
|
||||
final double verticalPadding = isCompact ? 12 : 16;
|
||||
final double gapSize = isCompact ? 4 : 8;
|
||||
final double rowSpacing = 12;
|
||||
|
||||
final cardContent = Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'levelingProgressLevel'.tr(args: [level.toString()]),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).fontSize(13).bold(),
|
||||
Text(
|
||||
'levelingProgressExperience'.tr(args: [experience.toString()]),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
).fontSize(13),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
Tooltip(
|
||||
message: '${(progress).toStringAsFixed(1)}%',
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 4,
|
||||
value: progress / 100,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
stageColor.withOpacity(0.1),
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
spacing: rowSpacing,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'levelingProgressLevel'.tr(args: [level.toString()]),
|
||||
style: TextStyle(
|
||||
color: stageColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: levelFontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'levelingStage$stage'.tr(),
|
||||
style: TextStyle(
|
||||
color: stageColor.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: stageFontSize,
|
||||
),
|
||||
),
|
||||
if (onTap != null) ...[
|
||||
const Gap(4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: isCompact ? 10 : 12,
|
||||
color: stageColor.withOpacity(0.7),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Gap(gapSize),
|
||||
Row(
|
||||
spacing: rowSpacing,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: '${progress.toStringAsFixed(1)}%',
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: progressHeight,
|
||||
value: progress,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow.withOpacity(0.75),
|
||||
color: stageColor,
|
||||
stopIndicatorRadius: 0,
|
||||
trackGap: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'levelingProgressExperience'.tr(
|
||||
args: [experience.toString()],
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.8),
|
||||
fontSize: experienceFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: horizontalPadding, vertical: verticalPadding),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return cardContent;
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/account/profile.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/activity_utils.dart';
|
||||
import 'package:island/widgets/account/status_creation.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -13,8 +15,31 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'status.g.dart';
|
||||
|
||||
class CurrentAccountStatusNotifier extends StateNotifier<SnAccountStatus?> {
|
||||
CurrentAccountStatusNotifier() : super(null);
|
||||
|
||||
void setStatus(SnAccountStatus status) {
|
||||
state = status;
|
||||
}
|
||||
|
||||
void clearStatus() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
|
||||
final currentAccountStatusProvider = StateNotifierProvider<CurrentAccountStatusNotifier, SnAccountStatus?>((ref) {
|
||||
return CurrentAccountStatusNotifier();
|
||||
});
|
||||
|
||||
@riverpod
|
||||
Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async {
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
if (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name)) {
|
||||
final local = ref.watch(currentAccountStatusProvider);
|
||||
if (local != null) {
|
||||
return local;
|
||||
}
|
||||
}
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
try {
|
||||
final resp = await apiClient.get('/id/accounts/$uname/statuses');
|
||||
@@ -110,7 +135,11 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final status = ref.watch(accountStatusProvider(uname));
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
final localStatus = ref.watch(currentAccountStatusProvider);
|
||||
final status = (uname == 'me' || (userInfo.value != null && uname == userInfo.value!.name && localStatus != null))
|
||||
? AsyncValue.data(localStatus)
|
||||
: ref.watch(accountStatusProvider(uname));
|
||||
final account = ref.watch(accountProvider(uname));
|
||||
|
||||
return Padding(
|
||||
@@ -133,10 +162,31 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
).padding(right: 4),
|
||||
if (status.value?.isCustomized ?? false)
|
||||
Flexible(
|
||||
child: Text(
|
||||
status.value?.label ?? 'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Activity Details'),
|
||||
content: buildActivityDetails(status.value),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Tooltip(
|
||||
richMessage: getActivityFullMessage(status.value),
|
||||
child: Text(
|
||||
getActivityTitle(status.value?.label, status.value?.meta) ??
|
||||
'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
@@ -148,7 +198,13 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
),
|
||||
if (!(status.value?.isOnline ?? false) &&
|
||||
if (getActivitySubtitle(status.value?.meta) != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
getActivitySubtitle(status.value?.meta)!,
|
||||
).opacity(0.75),
|
||||
)
|
||||
else if (!(status.value?.isOnline ?? false) &&
|
||||
account.value?.profile.lastSeenAt != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
|
363
lib/widgets/attachment_uploader.dart
Normal file
363
lib/widgets/attachment_uploader.dart
Normal file
@@ -0,0 +1,363 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/file_pool.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class AttachmentUploadConfig {
|
||||
final String poolId;
|
||||
final bool hasConstraints;
|
||||
|
||||
const AttachmentUploadConfig({
|
||||
required this.poolId,
|
||||
required this.hasConstraints,
|
||||
});
|
||||
}
|
||||
|
||||
class AttachmentUploaderSheet extends StatefulWidget {
|
||||
final WidgetRef ref;
|
||||
final ComposeState state;
|
||||
final int index;
|
||||
|
||||
const AttachmentUploaderSheet({
|
||||
super.key,
|
||||
required this.ref,
|
||||
required this.state,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentUploaderSheet> createState() =>
|
||||
_AttachmentUploaderSheetState();
|
||||
}
|
||||
|
||||
class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
|
||||
String? selectedPoolId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final attachment = widget.state.attachments.value[widget.index];
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'uploadAttachment'.tr(),
|
||||
child: FutureBuilder<List<SnFilePool>>(
|
||||
future: widget.ref.read(poolsProvider.future),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('errorLoadingPools'.tr()));
|
||||
}
|
||||
final pools = snapshot.data!;
|
||||
selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedPoolId,
|
||||
items:
|
||||
pools.map((pool) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: pool.id,
|
||||
child: Text(pool.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPoolId = value;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'selectPool'.tr(),
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'choosePool'.tr(),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
FutureBuilder<int?>(
|
||||
future: _getFileSize(attachment),
|
||||
builder: (context, sizeSnapshot) {
|
||||
if (!sizeSnapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final fileSize = sizeSnapshot.data!;
|
||||
final selectedPool = pools.firstWhere(
|
||||
(p) => p.id == selectedPoolId,
|
||||
);
|
||||
|
||||
// Check file size limit
|
||||
final maxFileSize =
|
||||
selectedPool.policyConfig?['max_file_size']
|
||||
as int?;
|
||||
final fileSizeExceeded =
|
||||
maxFileSize != null && fileSize > maxFileSize;
|
||||
|
||||
// Check accepted types
|
||||
final acceptTypes =
|
||||
selectedPool.policyConfig?['accept_types']
|
||||
as List?;
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(
|
||||
attachment.type,
|
||||
);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any(
|
||||
(type) => mimeType.startsWith(type),
|
||||
);
|
||||
|
||||
final hasIssues = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasIssues) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.warning,
|
||||
size: 18,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'uploadConstraints'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (fileSizeExceeded) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fileSizeExceeded'.tr(
|
||||
args: [
|
||||
_formatFileSize(maxFileSize),
|
||||
],
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!typeAccepted) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'fileTypeNotAccepted'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
],
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.account_balance_wallet,
|
||||
size: 18,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'quotaCostInfo'.tr(
|
||||
args: [
|
||||
_formatQuotaCost(
|
||||
fileSize,
|
||||
selectedPool,
|
||||
),
|
||||
],
|
||||
),
|
||||
style:
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium,
|
||||
).fontSize(13),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 4),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
const Icon(Symbols.info, size: 18),
|
||||
Text(
|
||||
'attachmentPreview'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).fontSize(13),
|
||||
],
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
AttachmentPreview(item: attachment, isCompact: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Symbols.close),
|
||||
label: Text('cancel').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmUpload(),
|
||||
icon: const Icon(Symbols.upload),
|
||||
label: Text('upload').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<AttachmentUploadConfig?> _getUploadConfig() async {
|
||||
final attachment = widget.state.attachments.value[widget.index];
|
||||
final fileSize = await _getFileSize(attachment);
|
||||
|
||||
if (fileSize == null) return null;
|
||||
|
||||
// Get the selected pool to check constraints
|
||||
final pools = await widget.ref.read(poolsProvider.future);
|
||||
final selectedPool = pools.firstWhere((p) => p.id == selectedPoolId);
|
||||
|
||||
// Check constraints
|
||||
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
|
||||
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
|
||||
|
||||
final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?;
|
||||
final mimeType =
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(attachment.type);
|
||||
final typeAccepted =
|
||||
acceptTypes == null ||
|
||||
acceptTypes.isEmpty ||
|
||||
acceptTypes.any((type) => mimeType.startsWith(type));
|
||||
|
||||
final hasConstraints = fileSizeExceeded || !typeAccepted;
|
||||
|
||||
return AttachmentUploadConfig(
|
||||
poolId: selectedPoolId!,
|
||||
hasConstraints: hasConstraints,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmUpload() async {
|
||||
final config = await _getUploadConfig();
|
||||
if (config != null && mounted) {
|
||||
Navigator.pop(context, config);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _getFileSize(UniversalFile attachment) async {
|
||||
if (attachment.data is XFile) {
|
||||
try {
|
||||
return await (attachment.data as XFile).length();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else if (attachment.data is SnCloudFile) {
|
||||
return (attachment.data as SnCloudFile).size;
|
||||
} else if (attachment.data is List<int>) {
|
||||
return (attachment.data as List<int>).length;
|
||||
} else if (attachment.data is Uint8List) {
|
||||
return (attachment.data as Uint8List).length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _formatNumber(int number) {
|
||||
if (number >= 1000000) {
|
||||
return '${(number / 1000000).toStringAsFixed(1)}M';
|
||||
} else if (number >= 1000) {
|
||||
return '${(number / 1000).toStringAsFixed(1)}K';
|
||||
} else {
|
||||
return number.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes >= 1073741824) {
|
||||
return '${(bytes / 1073741824).toStringAsFixed(1)} GB';
|
||||
} else if (bytes >= 1048576) {
|
||||
return '${(bytes / 1048576).toStringAsFixed(1)} MB';
|
||||
} else if (bytes >= 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else {
|
||||
return '$bytes bytes';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatQuotaCost(int fileSize, SnFilePool pool) {
|
||||
final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0;
|
||||
final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round();
|
||||
return _formatNumber(quotaCost);
|
||||
}
|
||||
}
|
330
lib/widgets/chat/chat_input.dart
Normal file
330
lib/widgets/chat/chat_input.dart
Normal file
@@ -0,0 +1,330 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:image_picker/image_picker.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/widgets/content/attachment_preview.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
import "package:pasteboard/pasteboard.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
import "package:island/widgets/stickers/picker.dart";
|
||||
|
||||
class ChatInput extends HookConsumerWidget {
|
||||
final TextEditingController messageController;
|
||||
final SnChatRoom chatRoom;
|
||||
final VoidCallback onSend;
|
||||
final VoidCallback onClear;
|
||||
final Function(bool isPhoto) onPickFile;
|
||||
final SnChatMessage? messageReplyingTo;
|
||||
final SnChatMessage? messageForwardingTo;
|
||||
final SnChatMessage? messageEditingTo;
|
||||
final List<UniversalFile> attachments;
|
||||
final Function(int) onUploadAttachment;
|
||||
final Function(int) onDeleteAttachment;
|
||||
final Function(int, int) onMoveAttachment;
|
||||
final Function(List<UniversalFile>) onAttachmentsChanged;
|
||||
final Map<String, Map<int, double>> attachmentProgress;
|
||||
|
||||
const ChatInput({
|
||||
super.key,
|
||||
required this.messageController,
|
||||
required this.chatRoom,
|
||||
required this.onSend,
|
||||
required this.onClear,
|
||||
required this.onPickFile,
|
||||
required this.messageReplyingTo,
|
||||
required this.messageForwardingTo,
|
||||
required this.messageEditingTo,
|
||||
required this.attachments,
|
||||
required this.onUploadAttachment,
|
||||
required this.onDeleteAttachment,
|
||||
required this.onMoveAttachment,
|
||||
required this.onAttachmentsChanged,
|
||||
required this.attachmentProgress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final inputFocusNode = useFocusNode();
|
||||
|
||||
final enterToSend = ref.watch(appSettingsNotifierProvider).enterToSend;
|
||||
|
||||
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
|
||||
void send() {
|
||||
onSend.call();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
inputFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handlePaste() async {
|
||||
final clipboard = await Pasteboard.image;
|
||||
if (clipboard == null) return;
|
||||
|
||||
onAttachmentsChanged([
|
||||
...attachments,
|
||||
UniversalFile(
|
||||
data: XFile.fromData(clipboard, mimeType: "image/jpeg"),
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
void handleKeyPress(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
RawKeyEvent event,
|
||||
) {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
|
||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||
final isModifierPressed = event.isMetaPressed || event.isControlPressed;
|
||||
|
||||
if (isPaste && isModifierPressed) {
|
||||
handlePaste();
|
||||
return;
|
||||
}
|
||||
|
||||
final enterToSend = ref.read(appSettingsNotifierProvider).enterToSend;
|
||||
final isEnter = event.logicalKey == LogicalKeyboardKey.enter;
|
||||
|
||||
if (isEnter) {
|
||||
if (enterToSend && !isModifierPressed) {
|
||||
send();
|
||||
} else if (!enterToSend && isModifierPressed) {
|
||||
send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Material(
|
||||
elevation: 8,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
if (attachments.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 280,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
height: 280,
|
||||
width: 280,
|
||||
child: AttachmentPreview(
|
||||
item: attachments[idx],
|
||||
progress: attachmentProgress['chat-upload']?[idx],
|
||||
onRequestUpload: () => onUploadAttachment(idx),
|
||||
onDelete: () => onDeleteAttachment(idx),
|
||||
onUpdate: (value) {
|
||||
attachments[idx] = value;
|
||||
onAttachmentsChanged(attachments);
|
||||
},
|
||||
onMove: (delta) => onMoveAttachment(idx, delta),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
),
|
||||
).padding(top: 12),
|
||||
if (messageReplyingTo != null ||
|
||||
messageForwardingTo != null ||
|
||||
messageEditingTo != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 8, right: 8, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
messageReplyingTo != null
|
||||
? Symbols.reply
|
||||
: messageForwardingTo != null
|
||||
? Symbols.forward
|
||||
: Symbols.edit,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
messageReplyingTo != null
|
||||
? 'Replying to ${messageReplyingTo?.sender.account.nick}'
|
||||
: messageForwardingTo != null
|
||||
? 'Forwarding message'
|
||||
: 'Editing message',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: onClear,
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
minimumSize: WidgetStatePropertyAll(Size(28, 28)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'stickers'.tr(),
|
||||
icon: const Icon(Symbols.add_reaction),
|
||||
onPressed: () {
|
||||
final size = MediaQuery.of(context).size;
|
||||
showStickerPickerPopover(
|
||||
context,
|
||||
Offset(
|
||||
20,
|
||||
size.height -
|
||||
480 -
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
onPick: (placeholder) {
|
||||
// Insert placeholder at current cursor position
|
||||
final text = messageController.text;
|
||||
final selection = messageController.selection;
|
||||
final start =
|
||||
selection.start >= 0
|
||||
? selection.start
|
||||
: text.length;
|
||||
final end =
|
||||
selection.end >= 0
|
||||
? selection.end
|
||||
: text.length;
|
||||
final newText = text.replaceRange(
|
||||
start,
|
||||
end,
|
||||
placeholder,
|
||||
);
|
||||
messageController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: start + placeholder.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Symbols.photo_library),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(true),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.photo),
|
||||
Text('addPhoto').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => onPickFile(false),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(Symbols.video_call),
|
||||
Text('addVideo').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (event) => handleKeyPress(context, ref, event),
|
||||
child: TextField(
|
||||
focusNode: inputFocusNode,
|
||||
controller: messageController,
|
||||
onSubmitted:
|
||||
(enterToSend && isMobile)
|
||||
? (_) {
|
||||
send();
|
||||
}
|
||||
: null,
|
||||
keyboardType:
|
||||
(enterToSend && isMobile)
|
||||
? TextInputType.text
|
||||
: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.send,
|
||||
inputFormatters: [
|
||||
if (enterToSend && !isMobile)
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
if (newValue.text.endsWith('\n')) {
|
||||
return oldValue;
|
||||
}
|
||||
return newValue;
|
||||
}),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
(chatRoom.type == 1 && chatRoom.name == null)
|
||||
? 'chatDirectMessageHint'.tr(
|
||||
args: [
|
||||
chatRoom.members!
|
||||
.map((e) => e.account.nick)
|
||||
.join(', '),
|
||||
],
|
||||
)
|
||||
: 'chatMessageHint'.tr(args: [chatRoom.name!]),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
counterText:
|
||||
messageController.text.length > 1024
|
||||
? '${messageController.text.length}/4096'
|
||||
: null,
|
||||
),
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: send,
|
||||
),
|
||||
],
|
||||
).padding(bottom: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
172
lib/widgets/chat/message_content.dart
Normal file
172
lib/widgets/chat/message_content.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:pretty_diff_text/pretty_diff_text.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class MessageContent extends StatelessWidget {
|
||||
final SnChatMessage item;
|
||||
final String? translatedText;
|
||||
|
||||
const MessageContent({super.key, required this.item, this.translatedText});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.type == 'messages.delete' || item.deletedAt != null) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.delete,
|
||||
size: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
item.content ?? 'Deleted a message',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 13,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case 'call.start':
|
||||
case 'call.ended':
|
||||
return _MessageContentCall(
|
||||
isEnded: item.type == 'call.ended',
|
||||
duration: item.meta['duration']?.toDouble(),
|
||||
);
|
||||
case 'messages.update':
|
||||
case 'messages.update.links':
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.edit,
|
||||
size: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
),
|
||||
const Gap(4),
|
||||
if (item.meta['previous_content'] is String)
|
||||
PrettyDiffText(
|
||||
oldText: item.meta['previous_content'],
|
||||
newText: item.content ?? 'Edited a message',
|
||||
defaultTextStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
addedTextStyle: TextStyle(
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryFixedDim.withOpacity(0.4),
|
||||
),
|
||||
deletedTextStyle: TextStyle(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.7),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
item.content ?? 'Edited a message',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
case 'text':
|
||||
default:
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MarkdownTextContent(
|
||||
content: item.content ?? '*${item.type} has no content*',
|
||||
isSelectable: true,
|
||||
linesMargin: EdgeInsets.zero,
|
||||
),
|
||||
if (translatedText?.isNotEmpty ?? false)
|
||||
...([
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.min(
|
||||
280,
|
||||
MediaQuery.of(context).size.width * 0.4,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('translated').tr().fontSize(11).opacity(0.75),
|
||||
const Gap(8),
|
||||
Flexible(child: Divider()),
|
||||
],
|
||||
).padding(vertical: 4),
|
||||
),
|
||||
MarkdownTextContent(
|
||||
content: translatedText!,
|
||||
isSelectable: true,
|
||||
linesMargin: EdgeInsets.zero,
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static bool hasContent(SnChatMessage item) {
|
||||
return item.type != 'text' || (item.content?.isNotEmpty ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
class _MessageContentCall extends StatelessWidget {
|
||||
final bool isEnded;
|
||||
final double? duration;
|
||||
|
||||
const _MessageContentCall({required this.isEnded, this.duration});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isEnded ? Symbols.call_end : Symbols.phone_in_talk,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Gap(4),
|
||||
Text(
|
||||
isEnded
|
||||
? 'Call ended after ${formatDuration(Duration(seconds: duration!.toInt()))}'
|
||||
: 'Call started',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
69
lib/widgets/chat/message_indicators.dart
Normal file
69
lib/widgets/chat/message_indicators.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class MessageIndicators extends StatelessWidget {
|
||||
final DateTime? editedAt;
|
||||
final MessageStatus? status;
|
||||
final bool isCurrentUser;
|
||||
final Color textColor;
|
||||
|
||||
const MessageIndicators({
|
||||
super.key,
|
||||
this.editedAt,
|
||||
this.status,
|
||||
required this.isCurrentUser,
|
||||
required this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
spacing: 4,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (editedAt != null)
|
||||
Text(
|
||||
'edited'.tr().toLowerCase(),
|
||||
style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)),
|
||||
),
|
||||
if (isCurrentUser && status != null)
|
||||
_buildStatusIcon(
|
||||
context,
|
||||
status!,
|
||||
textColor.withOpacity(0.7),
|
||||
).padding(bottom: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIcon(
|
||||
BuildContext context,
|
||||
MessageStatus status,
|
||||
Color textColor,
|
||||
) {
|
||||
switch (status) {
|
||||
case MessageStatus.pending:
|
||||
return Icon(Icons.access_time, size: 12, color: textColor);
|
||||
case MessageStatus.sent:
|
||||
return Icon(Icons.check, size: 12, color: textColor);
|
||||
case MessageStatus.failed:
|
||||
return Consumer(
|
||||
builder:
|
||||
(context, ref, _) => GestureDetector(
|
||||
onTap: () {
|
||||
// This would need to be passed in or accessed differently
|
||||
// For now, just show the error icon
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.error_outline,
|
||||
size: 12,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
87
lib/widgets/chat/message_list_tile.dart
Normal file
87
lib/widgets/chat/message_list_tile.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/utils/mapping.dart';
|
||||
import 'package:island/widgets/chat/message_content.dart';
|
||||
import 'package:island/widgets/chat/message_sender_info.dart';
|
||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/embed/link.dart';
|
||||
|
||||
class MessageListTile extends StatelessWidget {
|
||||
final LocalChatMessage message;
|
||||
final Function(String messageId) onJump;
|
||||
|
||||
const MessageListTile({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onJump,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final remoteMessage = message.toRemoteMessage();
|
||||
final sender = remoteMessage.sender;
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MessageSenderInfo(
|
||||
sender: sender,
|
||||
createdAt: message.createdAt,
|
||||
textColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
showAvatar: false,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MessageContent(item: remoteMessage),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (remoteMessage.attachments.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return CloudFileList(
|
||||
files: remoteMessage.attachments,
|
||||
maxWidth: constraints.maxWidth,
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (remoteMessage.meta['embeds'] != null)
|
||||
...((remoteMessage.meta['embeds'] as List<dynamic>)
|
||||
.map((embed) => convertMapKeysToSnakeCase(embed))
|
||||
.where((embed) => embed['type'] == 'link')
|
||||
.map((embed) => SnScrappedLink.fromJson(embed))
|
||||
.map(
|
||||
(link) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return EmbedLinkWidget(
|
||||
link: link,
|
||||
maxWidth: math.min(constraints.maxWidth, 480),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList()),
|
||||
],
|
||||
),
|
||||
onTap: () => onJump(message.id),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
dense: true,
|
||||
);
|
||||
}
|
||||
}
|
158
lib/widgets/chat/message_sender_info.dart
Normal file
158
lib/widgets/chat/message_sender_info.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/widgets/account/account_name.dart';
|
||||
import 'package:island/widgets/account/account_pfc.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
|
||||
class MessageSenderInfo extends StatelessWidget {
|
||||
final SnChatMember sender;
|
||||
final DateTime createdAt;
|
||||
final Color textColor;
|
||||
final bool showAvatar;
|
||||
final bool isCompact;
|
||||
|
||||
const MessageSenderInfo({
|
||||
super.key,
|
||||
required this.sender,
|
||||
required this.createdAt,
|
||||
required this.textColor,
|
||||
this.showAvatar = true,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final timestamp =
|
||||
DateTime.now().difference(createdAt).inDays > 365
|
||||
? DateFormat('yyyy/MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateTime.now().difference(createdAt).inDays > 0
|
||||
? DateFormat('MM/dd HH:mm').format(createdAt.toLocal())
|
||||
: DateFormat('HH:mm').format(createdAt.toLocal());
|
||||
|
||||
if (isCompact) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
if (showAvatar)
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
radius: 14,
|
||||
),
|
||||
),
|
||||
if (showAvatar) const Gap(4),
|
||||
AccountName(
|
||||
account: sender.account,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(
|
||||
timestamp,
|
||||
style: TextStyle(fontSize: 10, color: textColor.withOpacity(0.7)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (showAvatar) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
radius: 14,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountName(
|
||||
account: sender.account,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
timestamp,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: textColor.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showAvatar)
|
||||
AccountPfcGestureDetector(
|
||||
uname: sender.account.name,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: sender.account.profile.picture?.id,
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(timestamp, style: TextStyle(fontSize: 10, color: textColor)),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 5,
|
||||
children: [
|
||||
AccountName(
|
||||
account: sender.account,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
223
lib/widgets/chat/public_room_preview.dart
Normal file
223
lib/widgets/chat/public_room_preview.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:go_router/go_router.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:gap/gap.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:island/widgets/app_scaffold.dart";
|
||||
import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/content/cloud_files.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
import "package:styled_widget/styled_widget.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
import "package:material_symbols_icons/symbols.dart";
|
||||
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
final SnChatRoom room;
|
||||
|
||||
const PublicRoomPreview({super.key, required this.id, required this.room});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final messages = ref.watch(messagesNotifierProvider(id));
|
||||
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
var isLoading = false;
|
||||
|
||||
// Add scroll listener for pagination
|
||||
useEffect(() {
|
||||
void onScroll() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(onScroll);
|
||||
return () => scrollController.removeListener(onScroll);
|
||||
}, [scrollController]);
|
||||
|
||||
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
|
||||
SuperListView.builder(
|
||||
listController: listController,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
controller: scrollController,
|
||||
reverse: true, // Show newest messages at the bottom
|
||||
itemCount: messageList.length,
|
||||
findChildIndexCallback: (key) {
|
||||
final valueKey = key as ValueKey;
|
||||
final messageId = valueKey.value as String;
|
||||
return messageList.indexWhere((m) => m.id == messageId);
|
||||
},
|
||||
extentEstimation: (_, _) => 40,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
final nextMessage =
|
||||
index < messageList.length - 1 ? messageList[index + 1] : null;
|
||||
final isLastInGroup =
|
||||
nextMessage == null ||
|
||||
nextMessage.senderId != message.senderId ||
|
||||
nextMessage.createdAt
|
||||
.difference(message.createdAt)
|
||||
.inMinutes
|
||||
.abs() >
|
||||
3;
|
||||
|
||||
return MessageItem(
|
||||
message: message,
|
||||
isCurrentUser: false, // User is not a member, so not current user
|
||||
onAction: null, // No actions allowed in preview mode
|
||||
onJump: (_) {}, // No jump functionality in preview
|
||||
progress: null,
|
||||
showAvatar: isLastInGroup,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final compactHeader = isWideScreen(context);
|
||||
|
||||
Widget comfortHeaderWidget() => Column(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(15),
|
||||
],
|
||||
);
|
||||
|
||||
Widget compactHeaderWidget() => Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26,
|
||||
width: 26,
|
||||
child:
|
||||
(room.type == 1 && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
room.members!
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id != null
|
||||
? ProfilePictureWidget(
|
||||
fileId: room.picture?.id,
|
||||
fallbackIcon: Symbols.chat,
|
||||
)
|
||||
: CircleAvatar(
|
||||
child: Text(
|
||||
room.name![0].toUpperCase(),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(room.type == 1 && room.name == null)
|
||||
? room.members!.map((e) => e.account.nick).join(', ')
|
||||
: room.name!,
|
||||
).fontSize(19),
|
||||
],
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
|
||||
automaticallyImplyLeading: false,
|
||||
toolbarHeight: compactHeader ? null : 64,
|
||||
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
context.pushNamed('chatDetail', pathParameters: {'id': id});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: messages.when(
|
||||
data:
|
||||
(messageList) =>
|
||||
messageList.isEmpty
|
||||
? Center(child: Text('No messages yet'.tr()))
|
||||
: chatMessageListWidget(messageList),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => messagesNotifier.loadInitial(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Join button at the bottom for public rooms
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post('/sphere/chat/${room.id}/members/me');
|
||||
ref.invalidate(chatroomIdentityProvider(id));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
},
|
||||
label: Text('chatJoin').tr(),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -229,6 +229,7 @@ class CheckInWidget extends HookConsumerWidget {
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
spacing: 4,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
|
@@ -470,7 +470,8 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
if (onRequestUpload != null)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () => onRequestUpload?.call(),
|
||||
onTap:
|
||||
item.isOnCloud ? null : () => onRequestUpload?.call(),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
@@ -17,11 +14,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
||||
import 'package:island/widgets/content/sensitive.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -361,284 +357,11 @@ class CloudFileZoomIn extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void showInfoSheet() {
|
||||
final theme = Theme.of(context);
|
||||
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'File Information',
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('mimeType').tr(),
|
||||
Text(
|
||||
item.mimeType ?? 'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 28, child: const VerticalDivider()),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('fileSize').tr(),
|
||||
Text(
|
||||
formatFileSize(item.size),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (item.hash != null)
|
||||
SizedBox(height: 28, child: const VerticalDivider()),
|
||||
if (item.hash != null)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('fileHash').tr(),
|
||||
Text(
|
||||
'${item.hash!.substring(0, 6)}...',
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
onLongPress: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: item.hash!),
|
||||
);
|
||||
showSnackBar('File hash copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.tag),
|
||||
title: Text('ID').tr(),
|
||||
subtitle: Text(
|
||||
item.id,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.id));
|
||||
showSnackBar('File ID copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.file_present),
|
||||
title: Text('Name').tr(),
|
||||
subtitle: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.name));
|
||||
showSnackBar('File name copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
if (exifData.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title: Text(
|
||||
'exifData'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...exifData.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key.contains('-')
|
||||
? entry.key.split('-').last
|
||||
: entry.key,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
'${entry.value}'.isNotEmpty
|
||||
? '${entry.value}'
|
||||
: 'N/A',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: '${entry.value}'),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (item.fileMeta != null && item.fileMeta!.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title: Text(
|
||||
'File Metadata',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...item.fileMeta!.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
jsonEncode(entry.value),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: jsonEncode(entry.value),
|
||||
),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (item.userMeta != null && item.userMeta!.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title: Text(
|
||||
'User Metadata',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...item.userMeta!.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
jsonEncode(entry.value),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: jsonEncode(entry.value),
|
||||
),
|
||||
);
|
||||
showSnackBar('Value copied to clipboard');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
builder: (context) => FileInfoSheet(item: item),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -55,7 +55,7 @@ class CloudFilePicker extends HookConsumerWidget {
|
||||
uploadPosition.value = idx;
|
||||
final file = files.value[idx];
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: file,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
|
298
lib/widgets/content/file_info_sheet.dart
Normal file
298
lib/widgets/content/file_info_sheet.dart
Normal file
@@ -0,0 +1,298 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/utils/format.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class FileInfoSheet extends StatelessWidget {
|
||||
final SnCloudFile item;
|
||||
|
||||
const FileInfoSheet({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'fileInfoTitle'.tr(),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('mimeType').tr(),
|
||||
Text(
|
||||
item.mimeType ?? 'unknown'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 28, child: const VerticalDivider()),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('fileSize').tr(),
|
||||
Text(
|
||||
formatFileSize(item.size),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (item.hash != null)
|
||||
SizedBox(height: 28, child: const VerticalDivider()),
|
||||
if (item.hash != null)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('fileHash').tr(),
|
||||
Text(
|
||||
'${item.hash!.substring(0, 6)}...',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(text: item.hash!));
|
||||
showSnackBar('fileHashCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.tag),
|
||||
title: Text('ID').tr(),
|
||||
subtitle: Text(
|
||||
item.id,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.id));
|
||||
showSnackBar('fileIdCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.file_present),
|
||||
title: Text('Name').tr(),
|
||||
subtitle: Text(
|
||||
item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.name));
|
||||
showSnackBar('fileNameCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
if (item.pool != null)
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.calendar_today),
|
||||
title: Text('File Pool').tr(),
|
||||
subtitle: Text(
|
||||
item.pool!.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: item.pool!.id));
|
||||
showSnackBar('fileNameCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
if (exifData.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
'exifData'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...exifData.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key.contains('-')
|
||||
? entry.key.split('-').last
|
||||
: entry.key,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
'${entry.value}'.isNotEmpty
|
||||
? '${entry.value}'
|
||||
: 'N/A',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: '${entry.value}'),
|
||||
);
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (item.fileMeta != null && item.fileMeta!.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
'fileMetadata'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...item.fileMeta!.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
jsonEncode(entry.value),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: jsonEncode(entry.value)),
|
||||
);
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (item.userMeta != null && item.userMeta!.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Theme(
|
||||
data: theme.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
'userMetadata'.tr(),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...item.userMeta!.entries.map(
|
||||
(entry) => ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
title:
|
||||
Text(
|
||||
entry.key,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).bold(),
|
||||
subtitle: Text(
|
||||
jsonEncode(entry.value),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: jsonEncode(entry.value)),
|
||||
);
|
||||
showSnackBar('valueCopied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -64,6 +64,7 @@ class DebugSheet extends HookConsumerWidget {
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'Debug',
|
||||
heightFactor: 0.6,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
|
@@ -30,10 +30,13 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
final selectedRenderer = useState<PostEmbedViewRenderer>(
|
||||
PostEmbedViewRenderer.webView,
|
||||
);
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final iframeController = useTextEditingController();
|
||||
|
||||
void clearForm() {
|
||||
uriController.clear();
|
||||
aspectRatioController.clear();
|
||||
iframeController.clear();
|
||||
selectedRenderer.value = PostEmbedViewRenderer.webView;
|
||||
}
|
||||
|
||||
@@ -77,6 +80,57 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void parseIframe() {
|
||||
final iframe = iframeController.text.trim();
|
||||
if (iframe.isEmpty) return;
|
||||
|
||||
final srcMatch = RegExp(r'src="([^"]*)"').firstMatch(iframe);
|
||||
final widthMatch = RegExp(r'width="([^"]*)"').firstMatch(iframe);
|
||||
final heightMatch = RegExp(r'height="([^"]*)"').firstMatch(iframe);
|
||||
|
||||
if (srcMatch != null) {
|
||||
uriController.text = srcMatch.group(1)!;
|
||||
}
|
||||
|
||||
if (widthMatch != null && heightMatch != null) {
|
||||
final w = double.tryParse(widthMatch.group(1)!);
|
||||
final h = double.tryParse(heightMatch.group(1)!);
|
||||
if (w != null && h != null && h != 0) {
|
||||
aspectRatioController.text = (w / h).toStringAsFixed(3);
|
||||
}
|
||||
}
|
||||
|
||||
tabController.animateTo(1);
|
||||
}
|
||||
|
||||
void deleteEmbed(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => AlertDialog(
|
||||
title: Text('deleteEmbed').tr(),
|
||||
content: Text('deleteEmbedConfirm').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ComposeLogic.deleteEmbedView(state);
|
||||
clearForm();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text('delete').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'embedView'.tr(),
|
||||
heightFactor: 0.7,
|
||||
@@ -85,7 +139,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
// Header with save button when editing
|
||||
if (currentEmbedView != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -97,187 +151,207 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: saveEmbedView,
|
||||
style: ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
child: Text('save'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tab bar
|
||||
TabBar(
|
||||
controller: tabController,
|
||||
tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
|
||||
),
|
||||
|
||||
// Content area
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Form fields
|
||||
TextField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'embedUri'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
// Auto tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: iframeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'iframeCode'.tr(),
|
||||
hintText: 'iframeCodeHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
maxLines: 5,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: aspectRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'aspectRatio'.tr(),
|
||||
hintText: '16/9 = 1.777',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: parseIframe,
|
||||
icon: const Icon(Symbols.auto_fix),
|
||||
label: Text('parseIframe'.tr()),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownButtonFormField2<PostEmbedViewRenderer>(
|
||||
value: selectedRenderer.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'renderer'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
items:
|
||||
PostEmbedViewRenderer.values.map((renderer) {
|
||||
return DropdownMenuItem(
|
||||
value: renderer,
|
||||
child: Text(renderer.name).tr(),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
selectedRenderer.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Current embed view display (when exists)
|
||||
if (currentEmbedView != null) ...[
|
||||
const Gap(32),
|
||||
Text(
|
||||
'currentEmbed'.tr(),
|
||||
style: theme.textTheme.titleMedium,
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
top: 4,
|
||||
),
|
||||
// Manual tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Form fields
|
||||
TextField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'embedUri'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: aspectRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'aspectRatio'.tr(),
|
||||
hintText: '16/9 = 1.777',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d*\.?\d*$'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownButtonFormField2<PostEmbedViewRenderer>(
|
||||
value: selectedRenderer.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'renderer'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
selectedItemBuilder: (context) {
|
||||
return PostEmbedViewRenderer.values.map((renderer) {
|
||||
return Text(renderer.name).tr();
|
||||
}).toList();
|
||||
},
|
||||
menuItemStyleData: MenuItemStyleData(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
items:
|
||||
PostEmbedViewRenderer.values.map((renderer) {
|
||||
return DropdownMenuItem(
|
||||
value: renderer,
|
||||
child: Text(
|
||||
renderer.name,
|
||||
).tr().padding(horizontal: 20),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
selectedRenderer.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Current embed view display (when exists)
|
||||
if (currentEmbedView != null) ...[
|
||||
const Gap(32),
|
||||
Text(
|
||||
'currentEmbed'.tr(),
|
||||
style: theme.textTheme.titleMedium,
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
top: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
currentEmbedView.renderer ==
|
||||
PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
color: colorScheme.primary,
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
currentEmbedView.renderer ==
|
||||
PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
currentEmbedView.uri,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () => deleteEmbed(context),
|
||||
tooltip: 'delete'.tr(),
|
||||
color: colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
currentEmbedView.uri,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Text(
|
||||
'aspectRatio'.tr(),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => AlertDialog(
|
||||
title: Text('deleteEmbed').tr(),
|
||||
content:
|
||||
Text('deleteEmbedConfirm').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() =>
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop(),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ComposeLogic.deleteEmbedView(
|
||||
state,
|
||||
);
|
||||
clearForm();
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
colorScheme.error,
|
||||
),
|
||||
child: Text('delete').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'delete'.tr(),
|
||||
color: colorScheme.error,
|
||||
const Gap(4),
|
||||
Text(
|
||||
currentEmbedView.aspectRatio != null
|
||||
? currentEmbedView.aspectRatio!
|
||||
.toStringAsFixed(2)
|
||||
: 'notSet'.tr(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'aspectRatio'.tr(),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
currentEmbedView.aspectRatio != null
|
||||
? currentEmbedView.aspectRatio!
|
||||
.toStringAsFixed(2)
|
||||
: 'notSet'.tr(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Save button for new embed
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: saveEmbedView,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('addEmbed'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: saveEmbedView,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('addEmbed'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@@ -19,6 +20,7 @@ import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/post/compose_link_attachments.dart';
|
||||
import 'package:island/widgets/post/compose_poll.dart';
|
||||
import 'package:island/widgets/post/compose_recorder.dart';
|
||||
import 'package:island/pods/file_pool.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
import 'dart:async';
|
||||
@@ -183,7 +185,7 @@ class ComposeLogic {
|
||||
if (attachment.data is! SnCloudFile) {
|
||||
try {
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
@@ -386,6 +388,30 @@ class ComposeLogic {
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> pickGeneralFile(WidgetRef ref, ComposeState state) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.any,
|
||||
allowMultiple: true,
|
||||
);
|
||||
if (result == null || result.count == 0) return;
|
||||
|
||||
final newFiles = <UniversalFile>[];
|
||||
|
||||
for (final f in result.files) {
|
||||
if (f.path == null) continue;
|
||||
|
||||
final mimeType =
|
||||
lookupMimeType(f.path!, headerBytes: f.bytes) ??
|
||||
'application/octet-stream';
|
||||
final xfile = XFile(f.path!, name: f.name, mimeType: mimeType);
|
||||
|
||||
final uf = UniversalFile(data: xfile, type: UniversalFileType.file);
|
||||
newFiles.add(uf);
|
||||
}
|
||||
|
||||
state.attachments.value = [...state.attachments.value, ...newFiles];
|
||||
}
|
||||
|
||||
static Future<void> pickPhotoMedia(WidgetRef ref, ComposeState state) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.image,
|
||||
@@ -479,8 +505,9 @@ class ComposeLogic {
|
||||
static Future<void> uploadAttachment(
|
||||
WidgetRef ref,
|
||||
ComposeState state,
|
||||
int index,
|
||||
) async {
|
||||
int index, {
|
||||
String? poolId, // For Unit Test
|
||||
}) async {
|
||||
final attachment = state.attachments.value[index];
|
||||
if (attachment.isOnCloud) return;
|
||||
|
||||
@@ -489,22 +516,34 @@ class ComposeLogic {
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
|
||||
try {
|
||||
// Update progress state
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
// Upload file to cloud
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
SnCloudFile? cloudFile;
|
||||
|
||||
final pools = await ref.read(poolsProvider.future);
|
||||
final selectedPoolId = resolveDefaultPoolId(ref, pools);
|
||||
|
||||
cloudFile =
|
||||
await putFileToCloud(
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachment.data.name ?? 'Post media',
|
||||
poolId: selectedPoolId,
|
||||
filename:
|
||||
attachment.data.name ??
|
||||
(attachment.type == UniversalFileType.file
|
||||
? 'General file'
|
||||
: 'Post media'),
|
||||
mimetype:
|
||||
attachment.data.mimeType ??
|
||||
getMimeTypeFromFileType(attachment.type),
|
||||
mode:
|
||||
attachment.type == UniversalFileType.file
|
||||
? FileUploadMode.generic
|
||||
: FileUploadMode.mediaSafe,
|
||||
onProgress: (progress, _) {
|
||||
state.attachmentProgress.value = {
|
||||
...state.attachmentProgress.value,
|
||||
@@ -517,14 +556,12 @@ class ComposeLogic {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
|
||||
// Update attachments list with cloud file
|
||||
final clone = List.of(state.attachments.value);
|
||||
clone[index] = UniversalFile(data: cloudFile, type: attachment.type);
|
||||
state.attachments.value = clone;
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
showErrorAlert(err.toString());
|
||||
} finally {
|
||||
// Clean up progress state
|
||||
state.attachmentProgress.value = {...state.attachmentProgress.value}
|
||||
..remove(index);
|
||||
}
|
||||
@@ -635,7 +672,7 @@ class ComposeLogic {
|
||||
try {
|
||||
state.submitting.value = true;
|
||||
|
||||
// Upload any local attachments first
|
||||
// pload any local attachments first
|
||||
await Future.wait(
|
||||
state.attachments.value
|
||||
.asMap()
|
||||
@@ -643,7 +680,6 @@ class ComposeLogic {
|
||||
.where((entry) => entry.value.isOnDevice)
|
||||
.map((entry) => uploadAttachment(ref, state, entry.key)),
|
||||
);
|
||||
|
||||
// Prepare API request
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final isNewPost = originalPost == null;
|
||||
|
@@ -25,6 +25,10 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
ComposeLogic.pickVideoMedia(ref, state);
|
||||
}
|
||||
|
||||
void pickGeneralFile() {
|
||||
ComposeLogic.pickGeneralFile(ref, state);
|
||||
}
|
||||
|
||||
void addAudio() {
|
||||
ComposeLogic.recordAudioMedia(ref, state, context);
|
||||
}
|
||||
@@ -78,69 +82,83 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: pickPhotoMedia,
|
||||
tooltip: 'addPhoto'.tr(),
|
||||
icon: const Icon(Symbols.add_a_photo),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickVideoMedia,
|
||||
tooltip: 'addVideo'.tr(),
|
||||
icon: const Icon(Symbols.videocam),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: addAudio,
|
||||
tooltip: 'addAudio'.tr(),
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
tooltip: 'linkAttachment'.tr(),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
// Poll button with visual state when a poll is linked
|
||||
ListenableBuilder(
|
||||
listenable: state.pollId,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: pickPoll,
|
||||
icon: const Icon(Symbols.how_to_vote),
|
||||
tooltip: 'poll'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.pollId.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: pickPhotoMedia,
|
||||
tooltip: 'addPhoto'.tr(),
|
||||
icon: const Icon(Symbols.add_a_photo),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Embed button with visual state when embed is present
|
||||
ListenableBuilder(
|
||||
listenable: state.embedView,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: showEmbedSheet,
|
||||
icon: const Icon(Symbols.web),
|
||||
tooltip: 'embedView'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.embedView.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
IconButton(
|
||||
onPressed: pickVideoMedia,
|
||||
tooltip: 'addVideo'.tr(),
|
||||
icon: const Icon(Symbols.videocam),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
IconButton(
|
||||
onPressed: addAudio,
|
||||
tooltip: 'addAudio'.tr(),
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickGeneralFile,
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
tooltip: 'linkAttachment'.tr(),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
// Poll button with visual state when a poll is linked
|
||||
ListenableBuilder(
|
||||
listenable: state.pollId,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: pickPoll,
|
||||
icon: const Icon(Symbols.how_to_vote),
|
||||
tooltip: 'poll'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.pollId.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Embed button with visual state when embed is present
|
||||
ListenableBuilder(
|
||||
listenable: state.embedView,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: showEmbedSheet,
|
||||
icon: const Icon(Symbols.iframe),
|
||||
tooltip: 'embedView'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.embedView.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (originalPost == null && state.isEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.draft),
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
@@ -58,8 +61,8 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
children: [
|
||||
Icon(
|
||||
embedView.renderer == PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
? Symbols.globe
|
||||
: Symbols.iframe,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
@@ -74,13 +77,13 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
InkWell(
|
||||
child: Icon(
|
||||
Symbols.open_in_new,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () async {
|
||||
onTap: () async {
|
||||
final uri = Uri.parse(embedView.uri);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(
|
||||
@@ -89,10 +92,6 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: 'Open in browser',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -106,6 +105,20 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
? Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
gestureRecognizers: {
|
||||
Factory<VerticalDragGestureRecognizer>(
|
||||
() => VerticalDragGestureRecognizer(),
|
||||
),
|
||||
Factory<HorizontalDragGestureRecognizer>(
|
||||
() => HorizontalDragGestureRecognizer(),
|
||||
),
|
||||
Factory<ScaleGestureRecognizer>(
|
||||
() => ScaleGestureRecognizer(),
|
||||
),
|
||||
Factory<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(),
|
||||
),
|
||||
},
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri(embedView.uri),
|
||||
),
|
||||
@@ -256,14 +269,14 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.play_arrow,
|
||||
fill: 1,
|
||||
size: 48,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(
|
||||
0.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap to load content',
|
||||
'viewEmbedLoadHint'.tr(),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant
|
||||
.withOpacity(0.6),
|
||||
|
@@ -31,6 +31,36 @@ import 'package:share_plus/share_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
|
||||
const kAvailableStickers = {
|
||||
'angry',
|
||||
'clap',
|
||||
'confuse',
|
||||
'pray',
|
||||
'thumb_up',
|
||||
'party',
|
||||
};
|
||||
|
||||
bool _getReactionImageAvailable(String symbol) {
|
||||
return kAvailableStickers.contains(symbol);
|
||||
}
|
||||
|
||||
Widget _buildReactionIcon(String symbol, double size, {double iconSize = 24}) {
|
||||
if (_getReactionImageAvailable(symbol)) {
|
||||
return Image.asset(
|
||||
'assets/images/stickers/$symbol.png',
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.bottomCenter,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
kReactionTemplates[symbol]?.icon ?? '',
|
||||
style: TextStyle(fontSize: iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostActionableItem extends HookConsumerWidget {
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
@@ -490,57 +520,66 @@ class PostItem extends HookConsumerWidget {
|
||||
trailing:
|
||||
isCompact
|
||||
? null
|
||||
: IconButton(
|
||||
icon:
|
||||
mostReaction == null
|
||||
? const Icon(Symbols.add_reaction)
|
||||
: Badge(
|
||||
label: Center(
|
||||
child: Text(
|
||||
'x${item.reactionsCount[mostReaction]}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
: SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: IconButton(
|
||||
icon:
|
||||
mostReaction == null
|
||||
? const Icon(Symbols.add_reaction)
|
||||
: Badge(
|
||||
label: Center(
|
||||
child: Text(
|
||||
'x${item.reactionsCount[mostReaction]}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
offset: const Offset(4, 20),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.75),
|
||||
textColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
child: _buildReactionIcon(
|
||||
mostReaction,
|
||||
32,
|
||||
).padding(
|
||||
bottom:
|
||||
_getReactionImageAvailable(mostReaction)
|
||||
? 2
|
||||
: 0,
|
||||
),
|
||||
),
|
||||
offset: const Offset(4, 20),
|
||||
backgroundColor: Theme.of(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
(item.reactionsMade[mostReaction] ?? false)
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.75),
|
||||
textColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
child: Text(
|
||||
kReactionTemplates[mostReaction]?.icon ?? '',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
(item.reactionsMade[mostReaction] ?? false)
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.5)
|
||||
: null,
|
||||
).colorScheme.primary.withOpacity(0.5)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (BuildContext context) {
|
||||
return _PostReactionSheet(
|
||||
reactionsCount: item.reactionsCount,
|
||||
reactionsMade: item.reactionsMade,
|
||||
onReact: (symbol, attitude) {
|
||||
reactPost(symbol, attitude);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -3,
|
||||
vertical: -3,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
builder: (BuildContext context) {
|
||||
return _PostReactionSheet(
|
||||
reactionsCount: item.reactionsCount,
|
||||
reactionsMade: item.reactionsMade,
|
||||
onReact: (symbol, attitude) {
|
||||
reactPost(symbol, attitude);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -3,
|
||||
vertical: -3,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -611,7 +650,7 @@ class PostReactionList extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
@@ -649,7 +688,7 @@ class PostReactionList extends HookConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: Text(kReactionTemplates[symbol]?.icon ?? '?'),
|
||||
avatar: _buildReactionIcon(symbol, 24),
|
||||
label: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
@@ -786,37 +825,96 @@ class _PostReactionSheet extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final symbol = allReactions[index];
|
||||
final count = reactionsCount[symbol] ?? 0;
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
color:
|
||||
(reactionsMade[symbol] ?? false)
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerLowest,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
onReact(symbol, attitude);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
kReactionTemplates[symbol]?.icon ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
).fontSize(24),
|
||||
Text(
|
||||
ReactInfo.getTranslationKey(symbol),
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
if (count > 0)
|
||||
Text(
|
||||
'x$count',
|
||||
textAlign: TextAlign.center,
|
||||
).bold().padding(bottom: 4)
|
||||
else
|
||||
const Gap(20),
|
||||
],
|
||||
final hasImage = _getReactionImageAvailable(symbol);
|
||||
return Badge(
|
||||
label: Text('x$count'),
|
||||
isLabelVisible: count > 0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
offset: Offset(0, 0),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLowest,
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
onReact(symbol, attitude);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration:
|
||||
hasImage
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/stickers/$symbol.png',
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter:
|
||||
(reactionsMade[symbol] ?? false)
|
||||
? ColorFilter.mode(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withOpacity(0.7),
|
||||
BlendMode.srcATop,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (hasImage)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest
|
||||
.withOpacity(0.7),
|
||||
Colors.transparent,
|
||||
],
|
||||
stops: [0.0, 0.3],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment:
|
||||
hasImage
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!hasImage) _buildReactionIcon(symbol, 36),
|
||||
Text(
|
||||
ReactInfo.getTranslationKey(symbol),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: hasImage ? Colors.white : null,
|
||||
shadows:
|
||||
hasImage
|
||||
? [
|
||||
const Shadow(
|
||||
blurRadius: 4,
|
||||
offset: Offset(0.5, 0.5),
|
||||
color: Colors.black,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
).tr(),
|
||||
if (hasImage) const Gap(4),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@@ -247,7 +247,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
|
||||
for (var idx = 0; idx < universalFiles.length; idx++) {
|
||||
final file = universalFiles[idx];
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
await putFileToCloud(
|
||||
fileData: file,
|
||||
atk: token,
|
||||
baseUrl: serverUrl,
|
||||
|
@@ -21,19 +21,19 @@ PODS:
|
||||
- Firebase/Messaging (12.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.2.0)
|
||||
- firebase_analytics (12.0.1):
|
||||
- firebase_analytics (12.0.2):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.2.0)
|
||||
- FlutterMacOS
|
||||
- firebase_core (4.1.0):
|
||||
- firebase_core (4.1.1):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- FlutterMacOS
|
||||
- firebase_crashlytics (5.0.1):
|
||||
- firebase_crashlytics (5.0.2):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- Firebase/Crashlytics (~> 12.2.0)
|
||||
- firebase_core
|
||||
- FlutterMacOS
|
||||
- firebase_messaging (16.0.1):
|
||||
- firebase_messaging (16.0.2):
|
||||
- Firebase/CoreOnly (~> 12.2.0)
|
||||
- Firebase/Messaging (~> 12.2.0)
|
||||
- firebase_core
|
||||
@@ -402,10 +402,10 @@ SPEC CHECKSUMS:
|
||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
|
||||
Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1
|
||||
firebase_analytics: efe6e51156f4565f3791d99072e8e3b0fcca0e91
|
||||
firebase_core: a8d3b82b0a87bd1d0ebc21e686b37e939c56e6e1
|
||||
firebase_crashlytics: fdbe67a1229a9e583ebf2b155541491aa83927bb
|
||||
firebase_messaging: 6fb526705903e2e56e38a6ff56b43668b052b01b
|
||||
firebase_analytics: 26346c2ccb9ba410c2f33d5d34c62e6369cbbf29
|
||||
firebase_core: 54fd706197e1779d510b297548eee74d3b39577c
|
||||
firebase_crashlytics: 3694b4aca0849f6919244d7bbbb40615f989f46b
|
||||
firebase_messaging: 658f1a6906d80faec2fb20e3aadb81af6b09e441
|
||||
FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8
|
||||
FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd
|
||||
FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5
|
||||
|
80
pubspec.lock
80
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "948f7d74f41dd6f2d563ea9f4c21d7ea764f8e047d2b24138974c19c24d37eb6"
|
||||
sha256: "23d16f00a2da8ffa997c782453c73867b0609bd90435195671a54de38a3566df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.61"
|
||||
version: "1.3.62"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -449,6 +449,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: diff_match_patch
|
||||
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -477,10 +485,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "6aaea757f53bb035e8a3baedf3d1d53a79d6549a6c13d84f7546509da9372c7c"
|
||||
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.28.1"
|
||||
version: "2.28.2"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -493,10 +501,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: b52bd710f809db11e25259d429d799d034ba1c5224ce6a73fe8419feb980d44c
|
||||
sha256: b7534bf320aac5213259aac120670ba67b63a1fd010505babc436ff86083818f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6"
|
||||
version: "0.2.7"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -621,90 +629,90 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: dde9d6a7b69b07551a77cfb913c81c64804f7602b07541328322c321e73f2a0e
|
||||
sha256: fce78440ab7b95563054039aac5e342088efed9dc009ac6f81d5cac07155d509
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
version: "12.0.2"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "4008d82a58edcbedec34a7b39f457eed24181cb9c89782c104828c42e4c859b2"
|
||||
sha256: "75bdcd2d2635c4cdcd7ec13727527751ddf2f9933e5bf1264a2387920246f3c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.2"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: db2a2e8803f5471a5f89b4abacae95ae27e0644f77526879fb81a2c1abc12b5f
|
||||
sha256: ed5767695b131cdd425ee6d49934dca80689d9df40609c0d0aa8907ee6f0f785
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0+1"
|
||||
version: "0.6.0+2"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "967dae9a65f69377beb9f4ab292ea63ce5befa1ce24682cab1b69ca4b7a46927"
|
||||
sha256: "4dd96f05015c0dcceaa47711394c32971aee70169625d5e2477e7676c01ce0ee"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.1.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb"
|
||||
sha256: "5873a370f0d232918e23a5a6137dbe4c2c47cf017301f4ea02d9d636e52f60f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "6.0.1"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: f7ee08febc1c4451588ce58ffcf28edaee857e9a196fee88b85deb889990094a
|
||||
sha256: "61a51037312dac781f713308903bb7a1762a7f92f7bc286a3a0947fb2a713b82"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: f2e175a967712ee1f616ab8843390891a315428ba497ce3d256d4c46f32db6f8
|
||||
sha256: a636096df0d2a4bc72397bfc669a4fffc8896016a58de1a6f45a49d9ba064f94
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
version: "5.0.2"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: b49b90af4a1fd8f30b58abd90af88371969bea51b62838a4f4e737c2098b725e
|
||||
sha256: "1ccad077a6fc7bace97d8eace263f42e66dc23a23a839de864a4f10ac4a7c264"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.12"
|
||||
version: "3.8.13"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: aad5dcdea5698499b70d74d5a53b1f6a9972f85f97225e4b7ac006dd8d4f9bac
|
||||
sha256: ba12ad0b600e0c939fbb9391e1cd3320a5b5dad5284276b9182fc21eb1e72c2b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.1"
|
||||
version: "16.0.2"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "825bc11767bf50a43dccf49b3026f847ec31d0f176139bfc48d662cc128b5014"
|
||||
sha256: b4bade67bfc09fcc56eb012b3fc72b59ca9e2259a34cdfb81b368169770ff536
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.1"
|
||||
version: "4.7.2"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: db8dbdd79921245c4de02407e33cae2d1868683be18a5ba948d2af5311e3ef5d
|
||||
sha256: "8ae4a00d178993feb79603cad324b53696375cbb78805e8eb603fe331866629d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
version: "4.0.2"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1185,10 +1193,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
|
||||
sha256: c752e2d08d088bf83742cb05bf83003f3e9d276ff1519b5c92f9d5e60e5ddd23
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.2.2"
|
||||
version: "16.2.4"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1885,6 +1893,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
pretty_diff_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pretty_diff_text
|
||||
sha256: "2d4decf2bb6dac14c4e7a2cbd2da9ae7e004ccadc1155c723a0ca20a5daa7c57"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
process_run:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2161,10 +2177,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.12"
|
||||
version: "2.4.13"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
18
pubspec.yaml
18
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 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: 3.2.0+132
|
||||
version: 3.2.0+134
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@@ -39,7 +39,7 @@ dependencies:
|
||||
flutter_hooks: ^0.21.3+1
|
||||
hooks_riverpod: ^2.6.1
|
||||
bitsdojo_window: ^0.1.6
|
||||
go_router: ^16.2.2
|
||||
go_router: ^16.2.4
|
||||
styled_widget: ^0.4.1
|
||||
shared_preferences: ^2.5.3
|
||||
flutter_riverpod: ^2.6.1
|
||||
@@ -79,13 +79,13 @@ dependencies:
|
||||
image_picker_android: ^0.8.13+3
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.1
|
||||
firebase_messaging: ^16.0.2
|
||||
flutter_udid: ^4.0.0
|
||||
firebase_core: ^4.1.0
|
||||
firebase_core: ^4.1.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2873.0
|
||||
drift: ^2.28.1
|
||||
drift_flutter: ^0.2.6
|
||||
drift: ^2.28.2
|
||||
drift_flutter: ^0.2.7
|
||||
path: ^1.9.1
|
||||
collection: ^1.19.1
|
||||
markdown_editor_plus: ^0.2.15
|
||||
@@ -135,8 +135,8 @@ dependencies:
|
||||
flutter_app_update: ^3.2.2
|
||||
archive: ^4.0.7
|
||||
process_run: ^1.2.4
|
||||
firebase_crashlytics: ^5.0.1
|
||||
firebase_analytics: ^12.0.1
|
||||
firebase_crashlytics: ^5.0.2
|
||||
firebase_analytics: ^12.0.2
|
||||
material_color_utilities: ^0.11.1
|
||||
screenshot: ^3.0.0
|
||||
flutter_card_swiper: ^7.0.2
|
||||
@@ -152,6 +152,7 @@ dependencies:
|
||||
win32: ^5.14.0
|
||||
ffi: ^2.1.4
|
||||
dart_ipc: ^1.0.1
|
||||
pretty_diff_text: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -188,6 +189,7 @@ flutter:
|
||||
- assets/i18n/
|
||||
- assets/images/
|
||||
- assets/images/oidc/
|
||||
- assets/images/stickers/
|
||||
- assets/icons/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
|
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "132"
|
||||
#define BuildNumber "134"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
@@ -49,4 +49,4 @@ Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postin
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian"
|
||||
Type: files; Name: "{group}\Solian.lnk" ;
|
||||
Type: files; Name: "{autodesktop}\Solian.lnk" ;
|
||||
Type: files; Name: "{autodesktop}\Solian.lnk" ;
|
||||
|
Reference in New Issue
Block a user