Compare commits

..

25 Commits

Author SHA1 Message Date
fb51d2076f 🗑️ Remove pfp decoration test code 2025-12-10 23:12:02 +08:00
d8485954fa Profile decoration 2025-12-10 23:11:46 +08:00
d7746d14e4 🚀 Launch 3.5.0+151 2025-12-06 21:52:30 +08:00
648d5225f6 🐛 Ensure mobile site management request permission 2025-12-06 21:48:16 +08:00
9d4d0f2e48 🐛 Fix inconsistence alert 2025-12-06 21:44:43 +08:00
fe386163f4 💄 Optimize designs in developer hub 2025-12-06 21:39:50 +08:00
ac2cee10e5 💄 Hub now shows loading stautus of publishers / developers 2025-12-06 21:28:19 +08:00
9c370647dd 🐛 Fix some bugs in creator hub 2025-12-06 21:26:00 +08:00
7516e197fe 💄 Fix post replies skeleton inconststent 2025-12-06 21:15:32 +08:00
71c372ab6c Prefer auto dispose riverpods 2025-12-06 21:13:25 +08:00
25f23f7f93 🐛 Fix serval bugs during the changes 2025-12-06 21:05:29 +08:00
51853698b9 🐛 Fix serval bugs 2025-12-06 20:53:24 +08:00
39ed5393ab 💄 Dedicated notification skeleton 2025-12-06 20:49:54 +08:00
782b3f1b08 🐛 Fix article edit shows the post edit sheet 2025-12-06 20:45:47 +08:00
3ef2f13dd3 💄 Redesign the post tags and categories page 2025-12-06 20:40:28 +08:00
36b0f55a47 🐛 Fix inconsistent of margin in post silver list 2025-12-06 20:24:54 +08:00
bc7a6e865e 🐛 Fix some issues 2025-12-06 20:20:54 +08:00
2ff60fc4ff 💫 List loading state switch animation 2025-12-06 19:54:34 +08:00
ea93aa144e 🐛 Fix some bugs in post search UI 2025-12-06 19:47:36 +08:00
e4cd0c99df 💄 Optimize skeleton effect 2025-12-06 19:01:40 +08:00
dff84dde58 Post list now supports initial filter to prevent some mismatch 2025-12-06 18:47:50 +08:00
16c7b7e764 ♻️ Refactored post loading 2025-12-06 18:20:47 +08:00
240509ceff 🚚 Update files layout of pods 2025-12-06 17:31:12 +08:00
91da9768c1 💄 Adjust the style of the post reply preview 2025-12-06 15:26:23 +08:00
60b8e2bcad 💄 Optimize post reply preview 2025-12-06 13:53:22 +08:00
108 changed files with 7237 additions and 6904 deletions

View File

@@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

View File

@@ -618,6 +618,7 @@
"tagsHint": "Enter tags, separated by commas", "tagsHint": "Enter tags, separated by commas",
"categories": "Categories", "categories": "Categories",
"categoriesHint": "Enter categories, separated by commas", "categoriesHint": "Enter categories, separated by commas",
"categoriesAndTags": "Categories & Tags",
"chatNotJoined": "You have not joined this chat yet.", "chatNotJoined": "You have not joined this chat yet.",
"chatUnableJoin": "You can't join this chat due to it's access control settings.", "chatUnableJoin": "You can't join this chat due to it's access control settings.",
"chatJoin": "Join the Chat", "chatJoin": "Join the Chat",

View File

@@ -1489,5 +1489,6 @@
"accountActivationAlert": "请记住激活您的账户", "accountActivationAlert": "请记住激活您的账户",
"accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。", "accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。",
"accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。", "accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。",
"accountActivationResend": "重新发送" "accountActivationResend": "重新发送",
"noFurtherData": "已经到底了"
} }

View File

@@ -257,6 +257,8 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- pointer_interceptor_ios (0.0.1): - pointer_interceptor_ios (0.0.1):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
@@ -351,6 +353,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`) - protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
@@ -458,6 +461,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/pasteboard/ios" :path: ".symlinks/plugins/pasteboard/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
pointer_interceptor_ios: pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios" :path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios: protocol_handler_ios:
@@ -539,6 +544,7 @@ SPEC CHECKSUMS:
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0 pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851

View File

@@ -264,8 +264,7 @@ class $ChatRoomsTable extends ChatRooms
ChatRoom map(Map<String, dynamic> data, {String? tablePrefix}) { ChatRoom map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ChatRoom( return ChatRoom(
id: id: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}id'], data['${effectivePrefix}id'],
)!, )!,
@@ -277,8 +276,7 @@ class $ChatRoomsTable extends ChatRooms
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}description'], data['${effectivePrefix}description'],
), ),
type: type: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}type'], data['${effectivePrefix}type'],
)!, )!,
@@ -310,13 +308,11 @@ class $ChatRoomsTable extends ChatRooms
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}account_id'], data['${effectivePrefix}account_id'],
), ),
createdAt: createdAt: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
)!, )!,
updatedAt: updatedAt: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'], data['${effectivePrefix}updated_at'],
)!, )!,
@@ -416,39 +412,31 @@ class ChatRoom extends DataClass implements Insertable<ChatRoom> {
return ChatRoomsCompanion( return ChatRoomsCompanion(
id: Value(id), id: Value(id),
name: name == null && nullToAbsent ? const Value.absent() : Value(name), name: name == null && nullToAbsent ? const Value.absent() : Value(name),
description: description: description == null && nullToAbsent
description == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(description), : Value(description),
type: Value(type), type: Value(type),
isPublic: isPublic: isPublic == null && nullToAbsent
isPublic == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(isPublic), : Value(isPublic),
isCommunity: isCommunity: isCommunity == null && nullToAbsent
isCommunity == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(isCommunity), : Value(isCommunity),
picture: picture: picture == null && nullToAbsent
picture == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(picture), : Value(picture),
background: background: background == null && nullToAbsent
background == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(background), : Value(background),
realmId: realmId: realmId == null && nullToAbsent
realmId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(realmId), : Value(realmId),
accountId: accountId: accountId == null && nullToAbsent
accountId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(accountId), : Value(accountId),
createdAt: Value(createdAt), createdAt: Value(createdAt),
updatedAt: Value(updatedAt), updatedAt: Value(updatedAt),
deletedAt: deletedAt: deletedAt == null && nullToAbsent
deletedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(deletedAt), : Value(deletedAt),
); );
@@ -530,15 +518,18 @@ class ChatRoom extends DataClass implements Insertable<ChatRoom> {
return ChatRoom( return ChatRoom(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name, name: data.name.present ? data.name.value : this.name,
description: description: data.description.present
data.description.present ? data.description.value : this.description, ? data.description.value
: this.description,
type: data.type.present ? data.type.value : this.type, type: data.type.present ? data.type.value : this.type,
isPublic: data.isPublic.present ? data.isPublic.value : this.isPublic, isPublic: data.isPublic.present ? data.isPublic.value : this.isPublic,
isCommunity: isCommunity: data.isCommunity.present
data.isCommunity.present ? data.isCommunity.value : this.isCommunity, ? data.isCommunity.value
: this.isCommunity,
picture: data.picture.present ? data.picture.value : this.picture, picture: data.picture.present ? data.picture.value : this.picture,
background: background: data.background.present
data.background.present ? data.background.value : this.background, ? data.background.value
: this.background,
realmId: data.realmId.present ? data.realmId.value : this.realmId, realmId: data.realmId.present ? data.realmId.value : this.realmId,
accountId: data.accountId.present ? data.accountId.value : this.accountId, accountId: data.accountId.present ? data.accountId.value : this.accountId,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
@@ -1044,18 +1035,15 @@ class $ChatMembersTable extends ChatMembers
ChatMember map(Map<String, dynamic> data, {String? tablePrefix}) { ChatMember map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ChatMember( return ChatMember(
id: id: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}id'], data['${effectivePrefix}id'],
)!, )!,
chatRoomId: chatRoomId: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}chat_room_id'], data['${effectivePrefix}chat_room_id'],
)!, )!,
accountId: accountId: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}account_id'], data['${effectivePrefix}account_id'],
)!, )!,
@@ -1069,8 +1057,7 @@ class $ChatMembersTable extends ChatMembers
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}nick'], data['${effectivePrefix}nick'],
), ),
notify: notify: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}notify'], data['${effectivePrefix}notify'],
)!, )!,
@@ -1086,13 +1073,11 @@ class $ChatMembersTable extends ChatMembers
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}timeout_until'], data['${effectivePrefix}timeout_until'],
), ),
createdAt: createdAt: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
)!, )!,
updatedAt: updatedAt: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}updated_at'], data['${effectivePrefix}updated_at'],
)!, )!,
@@ -1179,22 +1164,18 @@ class ChatMember extends DataClass implements Insertable<ChatMember> {
account: Value(account), account: Value(account),
nick: nick == null && nullToAbsent ? const Value.absent() : Value(nick), nick: nick == null && nullToAbsent ? const Value.absent() : Value(nick),
notify: Value(notify), notify: Value(notify),
joinedAt: joinedAt: joinedAt == null && nullToAbsent
joinedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(joinedAt), : Value(joinedAt),
breakUntil: breakUntil: breakUntil == null && nullToAbsent
breakUntil == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(breakUntil), : Value(breakUntil),
timeoutUntil: timeoutUntil: timeoutUntil == null && nullToAbsent
timeoutUntil == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(timeoutUntil), : Value(timeoutUntil),
createdAt: Value(createdAt), createdAt: Value(createdAt),
updatedAt: Value(updatedAt), updatedAt: Value(updatedAt),
deletedAt: deletedAt: deletedAt == null && nullToAbsent
deletedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(deletedAt), : Value(deletedAt),
); );
@@ -1269,17 +1250,18 @@ class ChatMember extends DataClass implements Insertable<ChatMember> {
ChatMember copyWithCompanion(ChatMembersCompanion data) { ChatMember copyWithCompanion(ChatMembersCompanion data) {
return ChatMember( return ChatMember(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
chatRoomId: chatRoomId: data.chatRoomId.present
data.chatRoomId.present ? data.chatRoomId.value : this.chatRoomId, ? data.chatRoomId.value
: this.chatRoomId,
accountId: data.accountId.present ? data.accountId.value : this.accountId, accountId: data.accountId.present ? data.accountId.value : this.accountId,
account: data.account.present ? data.account.value : this.account, account: data.account.present ? data.account.value : this.account,
nick: data.nick.present ? data.nick.value : this.nick, nick: data.nick.present ? data.nick.value : this.nick,
notify: data.notify.present ? data.notify.value : this.notify, notify: data.notify.present ? data.notify.value : this.notify,
joinedAt: data.joinedAt.present ? data.joinedAt.value : this.joinedAt, joinedAt: data.joinedAt.present ? data.joinedAt.value : this.joinedAt,
breakUntil: breakUntil: data.breakUntil.present
data.breakUntil.present ? data.breakUntil.value : this.breakUntil, ? data.breakUntil.value
timeoutUntil: : this.breakUntil,
data.timeoutUntil.present timeoutUntil: data.timeoutUntil.present
? data.timeoutUntil.value ? data.timeoutUntil.value
: this.timeoutUntil, : this.timeoutUntil,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
@@ -1695,7 +1677,8 @@ class $ChatMessagesTable extends ChatMessages
List<Map<String, dynamic>>, List<Map<String, dynamic>>,
String String
> >
attachments = GeneratedColumn<String>( attachments =
GeneratedColumn<String>(
'attachments', 'attachments',
aliasedName, aliasedName,
false, false,
@@ -1710,7 +1693,8 @@ class $ChatMessagesTable extends ChatMessages
List<Map<String, dynamic>>, List<Map<String, dynamic>>,
String String
> >
reactions = GeneratedColumn<String>( reactions =
GeneratedColumn<String>(
'reactions', 'reactions',
aliasedName, aliasedName,
false, false,
@@ -1882,18 +1866,15 @@ class $ChatMessagesTable extends ChatMessages
ChatMessage map(Map<String, dynamic> data, {String? tablePrefix}) { ChatMessage map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ChatMessage( return ChatMessage(
id: id: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}id'], data['${effectivePrefix}id'],
)!, )!,
roomId: roomId: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}room_id'], data['${effectivePrefix}room_id'],
)!, )!,
senderId: senderId: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}sender_id'], data['${effectivePrefix}sender_id'],
)!, )!,
@@ -1905,13 +1886,11 @@ class $ChatMessagesTable extends ChatMessages
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}nonce'], data['${effectivePrefix}nonce'],
), ),
data: data: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}data'], data['${effectivePrefix}data'],
)!, )!,
createdAt: createdAt: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], data['${effectivePrefix}created_at'],
)!, )!,
@@ -1933,8 +1912,7 @@ class $ChatMessagesTable extends ChatMessages
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}deleted_at'], data['${effectivePrefix}deleted_at'],
), ),
type: type: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}type'], data['${effectivePrefix}type'],
)!, )!,
@@ -2101,42 +2079,36 @@ class ChatMessage extends DataClass implements Insertable<ChatMessage> {
id: Value(id), id: Value(id),
roomId: Value(roomId), roomId: Value(roomId),
senderId: Value(senderId), senderId: Value(senderId),
content: content: content == null && nullToAbsent
content == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(content), : Value(content),
nonce: nonce: nonce == null && nullToAbsent
nonce == null && nullToAbsent ? const Value.absent() : Value(nonce), ? const Value.absent()
: Value(nonce),
data: Value(data), data: Value(data),
createdAt: Value(createdAt), createdAt: Value(createdAt),
status: Value(status), status: Value(status),
isDeleted: isDeleted: isDeleted == null && nullToAbsent
isDeleted == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(isDeleted), : Value(isDeleted),
updatedAt: updatedAt: updatedAt == null && nullToAbsent
updatedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(updatedAt), : Value(updatedAt),
deletedAt: deletedAt: deletedAt == null && nullToAbsent
deletedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(deletedAt), : Value(deletedAt),
type: Value(type), type: Value(type),
meta: Value(meta), meta: Value(meta),
membersMentioned: Value(membersMentioned), membersMentioned: Value(membersMentioned),
editedAt: editedAt: editedAt == null && nullToAbsent
editedAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(editedAt), : Value(editedAt),
attachments: Value(attachments), attachments: Value(attachments),
reactions: Value(reactions), reactions: Value(reactions),
repliedMessageId: repliedMessageId: repliedMessageId == null && nullToAbsent
repliedMessageId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(repliedMessageId), : Value(repliedMessageId),
forwardedMessageId: forwardedMessageId: forwardedMessageId == null && nullToAbsent
forwardedMessageId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(forwardedMessageId), : Value(forwardedMessageId),
); );
@@ -2245,12 +2217,10 @@ class ChatMessage extends DataClass implements Insertable<ChatMessage> {
editedAt: editedAt.present ? editedAt.value : this.editedAt, editedAt: editedAt.present ? editedAt.value : this.editedAt,
attachments: attachments ?? this.attachments, attachments: attachments ?? this.attachments,
reactions: reactions ?? this.reactions, reactions: reactions ?? this.reactions,
repliedMessageId: repliedMessageId: repliedMessageId.present
repliedMessageId.present
? repliedMessageId.value ? repliedMessageId.value
: this.repliedMessageId, : this.repliedMessageId,
forwardedMessageId: forwardedMessageId: forwardedMessageId.present
forwardedMessageId.present
? forwardedMessageId.value ? forwardedMessageId.value
: this.forwardedMessageId, : this.forwardedMessageId,
); );
@@ -2269,20 +2239,18 @@ class ChatMessage extends DataClass implements Insertable<ChatMessage> {
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
type: data.type.present ? data.type.value : this.type, type: data.type.present ? data.type.value : this.type,
meta: data.meta.present ? data.meta.value : this.meta, meta: data.meta.present ? data.meta.value : this.meta,
membersMentioned: membersMentioned: data.membersMentioned.present
data.membersMentioned.present
? data.membersMentioned.value ? data.membersMentioned.value
: this.membersMentioned, : this.membersMentioned,
editedAt: data.editedAt.present ? data.editedAt.value : this.editedAt, editedAt: data.editedAt.present ? data.editedAt.value : this.editedAt,
attachments: attachments: data.attachments.present
data.attachments.present ? data.attachments.value : this.attachments, ? data.attachments.value
: this.attachments,
reactions: data.reactions.present ? data.reactions.value : this.reactions, reactions: data.reactions.present ? data.reactions.value : this.reactions,
repliedMessageId: repliedMessageId: data.repliedMessageId.present
data.repliedMessageId.present
? data.repliedMessageId.value ? data.repliedMessageId.value
: this.repliedMessageId, : this.repliedMessageId,
forwardedMessageId: forwardedMessageId: data.forwardedMessageId.present
data.forwardedMessageId.present
? data.forwardedMessageId.value ? data.forwardedMessageId.value
: this.forwardedMessageId, : this.forwardedMessageId,
); );
@@ -2809,8 +2777,7 @@ class $PostDraftsTable extends PostDrafts
PostDraft map(Map<String, dynamic> data, {String? tablePrefix}) { PostDraft map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return PostDraft( return PostDraft(
id: id: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}id'], data['${effectivePrefix}id'],
)!, )!,
@@ -2826,23 +2793,19 @@ class $PostDraftsTable extends PostDrafts
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}content'], data['${effectivePrefix}content'],
), ),
visibility: visibility: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}visibility'], data['${effectivePrefix}visibility'],
)!, )!,
type: type: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}type'], data['${effectivePrefix}type'],
)!, )!,
lastModified: lastModified: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}last_modified'], data['${effectivePrefix}last_modified'],
)!, )!,
postData: postData: attachedDatabase.typeMapping.read(
attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}post_data'], data['${effectivePrefix}post_data'],
)!, )!,
@@ -2897,14 +2860,13 @@ class PostDraft extends DataClass implements Insertable<PostDraft> {
PostDraftsCompanion toCompanion(bool nullToAbsent) { PostDraftsCompanion toCompanion(bool nullToAbsent) {
return PostDraftsCompanion( return PostDraftsCompanion(
id: Value(id), id: Value(id),
title: title: title == null && nullToAbsent
title == null && nullToAbsent ? const Value.absent() : Value(title), ? const Value.absent()
description: : Value(title),
description == null && nullToAbsent description: description == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(description), : Value(description),
content: content: content == null && nullToAbsent
content == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(content), : Value(content),
visibility: Value(visibility), visibility: Value(visibility),
@@ -2968,14 +2930,15 @@ class PostDraft extends DataClass implements Insertable<PostDraft> {
return PostDraft( return PostDraft(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
title: data.title.present ? data.title.value : this.title, title: data.title.present ? data.title.value : this.title,
description: description: data.description.present
data.description.present ? data.description.value : this.description, ? data.description.value
: this.description,
content: data.content.present ? data.content.value : this.content, content: data.content.present ? data.content.value : this.content,
visibility: visibility: data.visibility.present
data.visibility.present ? data.visibility.value : this.visibility, ? data.visibility.value
: this.visibility,
type: data.type.present ? data.type.value : this.type, type: data.type.present ? data.type.value : this.type,
lastModified: lastModified: data.lastModified.present
data.lastModified.present
? data.lastModified.value ? data.lastModified.value
: this.lastModified, : this.lastModified,
postData: data.postData.present ? data.postData.value : this.postData, postData: data.postData.present ? data.postData.value : this.postData,
@@ -3585,12 +3548,12 @@ class $$ChatRoomsTableTableManager
TableManagerState( TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: createFilteringComposer: () =>
() => $$ChatRoomsTableFilterComposer($db: db, $table: table), $$ChatRoomsTableFilterComposer($db: db, $table: table),
createOrderingComposer: createOrderingComposer: () =>
() => $$ChatRoomsTableOrderingComposer($db: db, $table: table), $$ChatRoomsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: createComputedFieldComposer: () =>
() => $$ChatRoomsTableAnnotationComposer($db: db, $table: table), $$ChatRoomsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
@@ -3655,9 +3618,7 @@ class $$ChatRoomsTableTableManager
deletedAt: deletedAt, deletedAt: deletedAt,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: withReferenceMapper: (p0) => p0
(p0) =>
p0
.map( .map(
(e) => ( (e) => (
e.readTable(table), e.readTable(table),
@@ -3665,10 +3626,8 @@ class $$ChatRoomsTableTableManager
), ),
) )
.toList(), .toList(),
prefetchHooksCallback: ({ prefetchHooksCallback:
chatMembersRefs = false, ({chatMembersRefs = false, chatMessagesRefs = false}) {
chatMessagesRefs = false,
}) {
return PrefetchHooks( return PrefetchHooks(
db: db, db: db,
explicitlyWatchedTables: [ explicitlyWatchedTables: [
@@ -3687,8 +3646,7 @@ class $$ChatRoomsTableTableManager
currentTable: table, currentTable: table,
referencedTable: $$ChatRoomsTableReferences referencedTable: $$ChatRoomsTableReferences
._chatMembersRefsTable(db), ._chatMembersRefsTable(db),
managerFromTypedResult: managerFromTypedResult: (p0) =>
(p0) =>
$$ChatRoomsTableReferences( $$ChatRoomsTableReferences(
db, db,
table, table,
@@ -3709,16 +3667,16 @@ class $$ChatRoomsTableTableManager
currentTable: table, currentTable: table,
referencedTable: $$ChatRoomsTableReferences referencedTable: $$ChatRoomsTableReferences
._chatMessagesRefsTable(db), ._chatMessagesRefsTable(db),
managerFromTypedResult: managerFromTypedResult: (p0) =>
(p0) =>
$$ChatRoomsTableReferences( $$ChatRoomsTableReferences(
db, db,
table, table,
p0, p0,
).chatMessagesRefs, ).chatMessagesRefs,
referencedItemsForCurrentItem: referencedItemsForCurrentItem:
(item, referencedItems) => (item, referencedItems) => referencedItems.where(
referencedItems.where((e) => e.roomId == item.id), (e) => e.roomId == item.id,
),
typedResults: items, typedResults: items,
), ),
]; ];
@@ -4142,12 +4100,11 @@ class $$ChatMembersTableTableManager
TableManagerState( TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: createFilteringComposer: () =>
() => $$ChatMembersTableFilterComposer($db: db, $table: table), $$ChatMembersTableFilterComposer($db: db, $table: table),
createOrderingComposer: createOrderingComposer: () =>
() => $$ChatMembersTableOrderingComposer($db: db, $table: table), $$ChatMembersTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: createComputedFieldComposer: () =>
() =>
$$ChatMembersTableAnnotationComposer($db: db, $table: table), $$ChatMembersTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
@@ -4209,9 +4166,7 @@ class $$ChatMembersTableTableManager
deletedAt: deletedAt, deletedAt: deletedAt,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: withReferenceMapper: (p0) => p0
(p0) =>
p0
.map( .map(
(e) => ( (e) => (
e.readTable(table), e.readTable(table),
@@ -4219,14 +4174,15 @@ class $$ChatMembersTableTableManager
), ),
) )
.toList(), .toList(),
prefetchHooksCallback: ({ prefetchHooksCallback:
chatRoomId = false, ({chatRoomId = false, chatMessagesRefs = false}) {
chatMessagesRefs = false,
}) {
return PrefetchHooks( return PrefetchHooks(
db: db, db: db,
explicitlyWatchedTables: [if (chatMessagesRefs) db.chatMessages], explicitlyWatchedTables: [
addJoins: < if (chatMessagesRefs) db.chatMessages,
],
addJoins:
<
T extends TableManagerState< T extends TableManagerState<
dynamic, dynamic,
dynamic, dynamic,
@@ -4246,7 +4202,8 @@ class $$ChatMembersTableTableManager
state.withJoin( state.withJoin(
currentTable: table, currentTable: table,
currentColumn: table.chatRoomId, currentColumn: table.chatRoomId,
referencedTable: $$ChatMembersTableReferences referencedTable:
$$ChatMembersTableReferences
._chatRoomIdTable(db), ._chatRoomIdTable(db),
referencedColumn: referencedColumn:
$$ChatMembersTableReferences $$ChatMembersTableReferences
@@ -4269,8 +4226,7 @@ class $$ChatMembersTableTableManager
currentTable: table, currentTable: table,
referencedTable: $$ChatMembersTableReferences referencedTable: $$ChatMembersTableReferences
._chatMessagesRefsTable(db), ._chatMessagesRefsTable(db),
managerFromTypedResult: managerFromTypedResult: (p0) =>
(p0) =>
$$ChatMembersTableReferences( $$ChatMembersTableReferences(
db, db,
table, table,
@@ -4831,12 +4787,11 @@ class $$ChatMessagesTableTableManager
TableManagerState( TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: createFilteringComposer: () =>
() => $$ChatMessagesTableFilterComposer($db: db, $table: table), $$ChatMessagesTableFilterComposer($db: db, $table: table),
createOrderingComposer: createOrderingComposer: () =>
() => $$ChatMessagesTableOrderingComposer($db: db, $table: table), $$ChatMessagesTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: createComputedFieldComposer: () =>
() =>
$$ChatMessagesTableAnnotationComposer($db: db, $table: table), $$ChatMessagesTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
@@ -4930,9 +4885,7 @@ class $$ChatMessagesTableTableManager
forwardedMessageId: forwardedMessageId, forwardedMessageId: forwardedMessageId,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: withReferenceMapper: (p0) => p0
(p0) =>
p0
.map( .map(
(e) => ( (e) => (
e.readTable(table), e.readTable(table),
@@ -4944,7 +4897,8 @@ class $$ChatMessagesTableTableManager
return PrefetchHooks( return PrefetchHooks(
db: db, db: db,
explicitlyWatchedTables: [], explicitlyWatchedTables: [],
addJoins: < addJoins:
<
T extends TableManagerState< T extends TableManagerState<
dynamic, dynamic,
dynamic, dynamic,
@@ -4966,8 +4920,7 @@ class $$ChatMessagesTableTableManager
currentColumn: table.roomId, currentColumn: table.roomId,
referencedTable: $$ChatMessagesTableReferences referencedTable: $$ChatMessagesTableReferences
._roomIdTable(db), ._roomIdTable(db),
referencedColumn: referencedColumn: $$ChatMessagesTableReferences
$$ChatMessagesTableReferences
._roomIdTable(db) ._roomIdTable(db)
.id, .id,
) )
@@ -4980,8 +4933,7 @@ class $$ChatMessagesTableTableManager
currentColumn: table.senderId, currentColumn: table.senderId,
referencedTable: $$ChatMessagesTableReferences referencedTable: $$ChatMessagesTableReferences
._senderIdTable(db), ._senderIdTable(db),
referencedColumn: referencedColumn: $$ChatMessagesTableReferences
$$ChatMessagesTableReferences
._senderIdTable(db) ._senderIdTable(db)
.id, .id,
) )
@@ -5201,12 +5153,12 @@ class $$PostDraftsTableTableManager
TableManagerState( TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: createFilteringComposer: () =>
() => $$PostDraftsTableFilterComposer($db: db, $table: table), $$PostDraftsTableFilterComposer($db: db, $table: table),
createOrderingComposer: createOrderingComposer: () =>
() => $$PostDraftsTableOrderingComposer($db: db, $table: table), $$PostDraftsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: createComputedFieldComposer: () =>
() => $$PostDraftsTableAnnotationComposer($db: db, $table: table), $$PostDraftsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
@@ -5251,15 +5203,8 @@ class $$PostDraftsTableTableManager
postData: postData, postData: postData,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: withReferenceMapper: (p0) => p0
(p0) => .map((e) => (e.readTable(table), BaseReferences(db, table, e)))
p0
.map(
(e) => (
e.readTable(table),
BaseReferences(db, table, e),
),
)
.toList(), .toList(),
prefetchHooksCallback: null, prefetchHooksCallback: null,
), ),

View File

@@ -12,16 +12,14 @@ _SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
resourceIdentifier: json['resource_identifier'] as String, resourceIdentifier: json['resource_identifier'] as String,
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
reason: json['reason'] as String, reason: json['reason'] as String,
resolvedAt: resolvedAt: json['resolved_at'] == null
json['resolved_at'] == null
? null ? null
: DateTime.parse(json['resolved_at'] as String), : DateTime.parse(json['resolved_at'] as String),
resolution: json['resolution'] as String?, resolution: json['resolution'] as String?,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -15,8 +15,7 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
isSuperuser: json['is_superuser'] as bool, isSuperuser: json['is_superuser'] as bool,
automatedId: json['automated_id'] as String?, automatedId: json['automated_id'] as String?,
profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
perkSubscription: perkSubscription: json['perk_subscription'] == null
json['perk_subscription'] == null
? null ? null
: SnWalletSubscriptionRef.fromJson( : SnWalletSubscriptionRef.fromJson(
json['perk_subscription'] as Map<String, dynamic>, json['perk_subscription'] as Map<String, dynamic>,
@@ -31,14 +30,12 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
?.map((e) => SnContactMethod.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnContactMethod.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
activatedAt: activatedAt: json['activated_at'] == null
json['activated_at'] == null
? null ? null
: DateTime.parse(json['activated_at'] as String), : DateTime.parse(json['activated_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -73,8 +70,9 @@ _UsernameColor _$UsernameColorFromJson(Map<String, dynamic> json) =>
type: json['type'] as String? ?? 'plain', type: json['type'] as String? ?? 'plain',
value: json['value'] as String?, value: json['value'] as String?,
direction: json['direction'] as String?, direction: json['direction'] as String?,
colors: colors: (json['colors'] as List<dynamic>?)
(json['colors'] as List<dynamic>?)?.map((e) => e as String).toList(), ?.map((e) => e as String)
.toList(),
); );
Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) => Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
@@ -85,8 +83,9 @@ Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
'colors': instance.colors, 'colors': instance.colors,
}; };
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => _SnAccountProfile _$SnAccountProfileFromJson(
_SnAccountProfile( Map<String, dynamic> json,
) => _SnAccountProfile(
id: json['id'] as String, id: json['id'] as String,
firstName: json['first_name'] as String? ?? '', firstName: json['first_name'] as String? ?? '',
middleName: json['middle_name'] as String? ?? '', middleName: json['middle_name'] as String? ?? '',
@@ -96,58 +95,43 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
pronouns: json['pronouns'] as String? ?? '', pronouns: json['pronouns'] as String? ?? '',
location: json['location'] as String? ?? '', location: json['location'] as String? ?? '',
timeZone: json['time_zone'] as String? ?? '', timeZone: json['time_zone'] as String? ?? '',
birthday: birthday: json['birthday'] == null
json['birthday'] == null
? null ? null
: DateTime.parse(json['birthday'] as String), : DateTime.parse(json['birthday'] as String),
links: links: json['links'] == null
json['links'] == null
? const [] ? const []
: const ProfileLinkConverter().fromJson(json['links']), : const ProfileLinkConverter().fromJson(json['links']),
lastSeenAt: lastSeenAt: json['last_seen_at'] == null
json['last_seen_at'] == null
? null ? null
: DateTime.parse(json['last_seen_at'] as String), : DateTime.parse(json['last_seen_at'] as String),
activeBadge: activeBadge: json['active_badge'] == null
json['active_badge'] == null
? null ? null
: SnAccountBadge.fromJson( : SnAccountBadge.fromJson(json['active_badge'] as Map<String, dynamic>),
json['active_badge'] as Map<String, dynamic>,
),
experience: (json['experience'] as num).toInt(), experience: (json['experience'] as num).toInt(),
level: (json['level'] as num).toInt(), level: (json['level'] as num).toInt(),
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100, socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0, socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
levelingProgress: (json['leveling_progress'] as num).toDouble(), levelingProgress: (json['leveling_progress'] as num).toDouble(),
picture: picture: json['picture'] == null
json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background: background: json['background'] == null
json['background'] == null
? null ? null
: SnCloudFile.fromJson( : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
json['background'] as Map<String, dynamic>, verification: json['verification'] == null
),
verification:
json['verification'] == null
? null ? null
: SnVerificationMark.fromJson( : SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>, json['verification'] as Map<String, dynamic>,
), ),
usernameColor: usernameColor: json['username_color'] == null
json['username_color'] == null
? null ? null
: UsernameColor.fromJson( : UsernameColor.fromJson(json['username_color'] as Map<String, dynamic>),
json['username_color'] as Map<String, dynamic>,
),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) => Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -188,15 +172,13 @@ _SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
isCustomized: json['is_customized'] as bool, isCustomized: json['is_customized'] as bool,
label: json['label'] as String? ?? "", label: json['label'] as String? ?? "",
meta: json['meta'] as Map<String, dynamic>?, meta: json['meta'] as Map<String, dynamic>?,
clearedAt: clearedAt: json['cleared_at'] == null
json['cleared_at'] == null
? null ? null
: DateTime.parse(json['cleared_at'] as String), : DateTime.parse(json['cleared_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -225,19 +207,16 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
label: json['label'] as String?, label: json['label'] as String?,
caption: json['caption'] as String?, caption: json['caption'] as String?,
meta: json['meta'] as Map<String, dynamic>, meta: json['meta'] as Map<String, dynamic>,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
activatedAt: activatedAt: json['activated_at'] == null
json['activated_at'] == null
? null ? null
: DateTime.parse(json['activated_at'] as String), : DateTime.parse(json['activated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -261,8 +240,7 @@ _SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
_SnContactMethod( _SnContactMethod(
id: json['id'] as String, id: json['id'] as String,
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
verifiedAt: verifiedAt: json['verified_at'] == null
json['verified_at'] == null
? null ? null
: DateTime.parse(json['verified_at'] as String), : DateTime.parse(json['verified_at'] as String),
isPrimary: json['is_primary'] as bool, isPrimary: json['is_primary'] as bool,
@@ -271,8 +249,7 @@ _SnContactMethod _$SnContactMethodFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -295,8 +272,7 @@ _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
_SnNotification( _SnNotification(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String, id: json['id'] as String,
@@ -306,8 +282,7 @@ _SnNotification _$SnNotificationFromJson(Map<String, dynamic> json) =>
content: json['content'] as String, content: json['content'] as String,
meta: json['meta'] as Map<String, dynamic>? ?? const {}, meta: json['meta'] as Map<String, dynamic>? ?? const {},
priority: (json['priority'] as num).toInt(), priority: (json['priority'] as num).toInt(),
viewedAt: viewedAt: json['viewed_at'] == null
json['viewed_at'] == null
? null ? null
: DateTime.parse(json['viewed_at'] as String), : DateTime.parse(json['viewed_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
@@ -376,8 +351,7 @@ _SnAuthDeviceWithSessione _$SnAuthDeviceWithSessioneFromJson(
deviceLabel: json['device_label'] as String?, deviceLabel: json['device_label'] as String?,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
platform: (json['platform'] as num).toInt(), platform: (json['platform'] as num).toInt(),
sessions: sessions: (json['sessions'] as List<dynamic>)
(json['sessions'] as List<dynamic>)
.map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>)) .map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
isCurrent: json['is_current'] as bool? ?? false, isCurrent: json['is_current'] as bool? ?? false,
@@ -405,8 +379,7 @@ _SnExperienceRecord _$SnExperienceRecordFromJson(Map<String, dynamic> json) =>
bonusMultiplier: (json['bonus_multiplier'] as num?)?.toDouble() ?? 1.0, bonusMultiplier: (json['bonus_multiplier'] as num?)?.toDouble() ?? 1.0,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -430,14 +403,12 @@ _SnSocialCreditRecord _$SnSocialCreditRecordFromJson(
delta: (json['delta'] as num).toDouble(), delta: (json['delta'] as num).toDouble(),
reasonType: json['reason_type'] as String, reasonType: json['reason_type'] as String,
reason: json['reason'] as String, reason: json['reason'] as String,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -460,8 +431,7 @@ _SnFriendOverviewItem _$SnFriendOverviewItemFromJson(
) => _SnFriendOverviewItem( ) => _SnFriendOverviewItem(
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>), account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
status: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>), status: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
activities: activities: (json['activities'] as List<dynamic>)
(json['activities'] as List<dynamic>)
.map((e) => SnPresenceActivity.fromJson(e as Map<String, dynamic>)) .map((e) => SnPresenceActivity.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );

View File

@@ -12,8 +12,7 @@ _SnNotableDay _$SnNotableDayFromJson(Map<String, dynamic> json) =>
localName: json['local_name'] as String, localName: json['local_name'] as String,
globalName: json['global_name'] as String, globalName: json['global_name'] as String,
countryCode: json['country_code'] as String, countryCode: json['country_code'] as String,
holidays: holidays: (json['holidays'] as List<dynamic>)
(json['holidays'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
); );
@@ -35,8 +34,7 @@ _SnTimelineEvent _$SnTimelineEventFromJson(Map<String, dynamic> json) =>
data: json['data'], data: json['data'],
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -56,19 +54,16 @@ _SnCheckInResult _$SnCheckInResultFromJson(Map<String, dynamic> json) =>
_SnCheckInResult( _SnCheckInResult(
id: json['id'] as String, id: json['id'] as String,
level: (json['level'] as num).toInt(), level: (json['level'] as num).toInt(),
tips: tips: (json['tips'] as List<dynamic>)
(json['tips'] as List<dynamic>)
.map((e) => SnFortuneTip.fromJson(e as Map<String, dynamic>)) .map((e) => SnFortuneTip.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -103,14 +98,12 @@ _SnEventCalendarEntry _$SnEventCalendarEntryFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => _SnEventCalendarEntry( ) => _SnEventCalendarEntry(
date: DateTime.parse(json['date'] as String), date: DateTime.parse(json['date'] as String),
checkInResult: checkInResult: json['check_in_result'] == null
json['check_in_result'] == null
? null ? null
: SnCheckInResult.fromJson( : SnCheckInResult.fromJson(
json['check_in_result'] as Map<String, dynamic>, json['check_in_result'] as Map<String, dynamic>,
), ),
statuses: statuses: (json['statuses'] as List<dynamic>)
(json['statuses'] as List<dynamic>)
.map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>)) .map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
@@ -141,8 +134,7 @@ _SnPresenceActivity _$SnPresenceActivityFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -34,15 +34,13 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
_SnAuthChallenge( _SnAuthChallenge(
id: json['id'] as String, id: json['id'] as String,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
stepRemain: (json['step_remain'] as num).toInt(), stepRemain: (json['step_remain'] as num).toInt(),
stepTotal: (json['step_total'] as num).toInt(), stepTotal: (json['step_total'] as num).toInt(),
failedAttempts: (json['failed_attempts'] as num).toInt(), failedAttempts: (json['failed_attempts'] as num).toInt(),
blacklistFactors: blacklistFactors: (json['blacklist_factors'] as List<dynamic>)
(json['blacklist_factors'] as List<dynamic>)
.map((e) => e as String) .map((e) => e as String)
.toList(), .toList(),
audiences: json['audiences'] as List<dynamic>, audiences: json['audiences'] as List<dynamic>,
@@ -50,17 +48,13 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
ipAddress: json['ip_address'] as String, ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] as String, userAgent: json['user_agent'] as String,
nonce: json['nonce'] as String?, nonce: json['nonce'] as String?,
location: location: json['location'] == null
json['location'] == null
? null ? null
: GeoIpLocation.fromJson( : GeoIpLocation.fromJson(json['location'] as Map<String, dynamic>),
json['location'] as Map<String, dynamic>,
),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -90,26 +84,21 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
id: json['id'] as String, id: json['id'] as String,
label: json['label'] as String?, label: json['label'] as String?,
lastGrantedAt: DateTime.parse(json['last_granted_at'] as String), lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
audiences: json['audiences'] as List<dynamic>, audiences: json['audiences'] as List<dynamic>,
scopes: json['scopes'] as List<dynamic>, scopes: json['scopes'] as List<dynamic>,
ipAddress: json['ip_address'] as String?, ipAddress: json['ip_address'] as String?,
userAgent: json['user_agent'] as String?, userAgent: json['user_agent'] as String?,
location: location: json['location'] == null
json['location'] == null
? null ? null
: GeoIpLocation.fromJson( : GeoIpLocation.fromJson(json['location'] as Map<String, dynamic>),
json['location'] as Map<String, dynamic>,
),
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -138,16 +127,13 @@ _SnAuthFactor _$SnAuthFactorFromJson(Map<String, dynamic> json) =>
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
enabledAt: enabledAt: json['enabled_at'] == null
json['enabled_at'] == null
? null ? null
: DateTime.parse(json['enabled_at'] as String), : DateTime.parse(json['enabled_at'] as String),
trustworthy: (json['trustworthy'] as num).toInt(), trustworthy: (json['trustworthy'] as num).toInt(),
@@ -177,8 +163,7 @@ _SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) =>
lastUsedAt: DateTime.parse(json['last_used_at'] as String), lastUsedAt: DateTime.parse(json['last_used_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -10,8 +10,7 @@ AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => AutoCompletionAccountResponse( ) => AutoCompletionAccountResponse(
type: json['type'] as String, type: json['type'] as String,
items: items: (json['items'] as List<dynamic>)
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>)) .map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,
@@ -29,8 +28,7 @@ AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => AutoCompletionStickerResponse( ) => AutoCompletionStickerResponse(
type: json['type'] as String, type: json['type'] as String,
items: items: (json['items'] as List<dynamic>)
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>)) .map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,

View File

@@ -14,8 +14,7 @@ _Bot _$BotFromJson(Map<String, dynamic> json) => _Bot(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>), account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
developer: developer: json['developer'] == null
json['developer'] == null
? null ? null
: SnDeveloper.fromJson(json['developer'] as Map<String, dynamic>), : SnDeveloper.fromJson(json['developer'] as Map<String, dynamic>),
); );
@@ -74,8 +73,7 @@ _BotSecret _$BotSecretFromJson(Map<String, dynamic> json) => _BotSecret(
id: json['id'] as String? ?? '', id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '', secret: json['secret'] as String? ?? '',
description: json['description'] as String?, description: json['description'] as String?,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
botId: json['bot_id'] as String? ?? '', botId: json['bot_id'] as String? ?? '',

View File

@@ -13,28 +13,23 @@ _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
isPublic: json['is_public'] as bool? ?? false, isPublic: json['is_public'] as bool? ?? false,
isCommunity: json['is_community'] as bool? ?? false, isCommunity: json['is_community'] as bool? ?? false,
picture: picture: json['picture'] == null
json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background: background: json['background'] == null
json['background'] == null
? null ? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
realmId: json['realm_id'] as String?, realmId: json['realm_id'] as String?,
accountId: json['account_id'] as String?, accountId: json['account_id'] as String?,
realm: realm: json['realm'] == null
json['realm'] == null
? null ? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>), : SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
members: members: (json['members'] as List<dynamic>?)
(json['members'] as List<dynamic>?)
?.map((e) => SnChatMember.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnChatMember.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
@@ -62,8 +57,7 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
_SnChatMessage( _SnChatMessage(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String, id: json['id'] as String,
@@ -76,8 +70,7 @@ _SnChatMessage _$SnChatMessageFromJson(Map<String, dynamic> json) =>
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
const [], const [],
editedAt: editedAt: json['edited_at'] == null
json['edited_at'] == null
? null ? null
: DateTime.parse(json['edited_at'] as String), : DateTime.parse(json['edited_at'] as String),
attachments: attachments:
@@ -122,8 +115,7 @@ _SnChatReaction _$SnChatReactionFromJson(Map<String, dynamic> json) =>
_SnChatReaction( _SnChatReaction(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String, id: json['id'] as String,
@@ -151,40 +143,31 @@ _SnChatMember _$SnChatMemberFromJson(Map<String, dynamic> json) =>
_SnChatMember( _SnChatMember(
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
id: json['id'] as String, id: json['id'] as String,
chatRoomId: json['chat_room_id'] as String, chatRoomId: json['chat_room_id'] as String,
chatRoom: chatRoom: json['chat_room'] == null
json['chat_room'] == null
? null ? null
: SnChatRoom.fromJson(json['chat_room'] as Map<String, dynamic>), : SnChatRoom.fromJson(json['chat_room'] as Map<String, dynamic>),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>), account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
nick: json['nick'] as String?, nick: json['nick'] as String?,
notify: (json['notify'] as num).toInt(), notify: (json['notify'] as num).toInt(),
joinedAt: joinedAt: json['joined_at'] == null
json['joined_at'] == null
? null ? null
: DateTime.parse(json['joined_at'] as String), : DateTime.parse(json['joined_at'] as String),
breakUntil: breakUntil: json['break_until'] == null
json['break_until'] == null
? null ? null
: DateTime.parse(json['break_until'] as String), : DateTime.parse(json['break_until'] as String),
timeoutUntil: timeoutUntil: json['timeout_until'] == null
json['timeout_until'] == null
? null ? null
: DateTime.parse(json['timeout_until'] as String), : DateTime.parse(json['timeout_until'] as String),
status: status: json['status'] == null
json['status'] == null
? null ? null
: SnAccountStatus.fromJson( : SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
json['status'] as Map<String, dynamic>, lastTyped: json['last_typed'] == null
),
lastTyped:
json['last_typed'] == null
? null ? null
: DateTime.parse(json['last_typed'] as String), : DateTime.parse(json['last_typed'] as String),
); );
@@ -211,8 +194,7 @@ Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
_SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) => _SnChatSummary _$SnChatSummaryFromJson(Map<String, dynamic> json) =>
_SnChatSummary( _SnChatSummary(
unreadCount: (json['unread_count'] as num).toInt(), unreadCount: (json['unread_count'] as num).toInt(),
lastMessage: lastMessage: json['last_message'] == null
json['last_message'] == null
? null ? null
: SnChatMessage.fromJson( : SnChatMessage.fromJson(
json['last_message'] as Map<String, dynamic>, json['last_message'] as Map<String, dynamic>,
@@ -251,8 +233,7 @@ _ChatRealtimeJoinResponse _$ChatRealtimeJoinResponseFromJson(
callId: json['call_id'] as String, callId: json['call_id'] as String,
roomName: json['room_name'] as String, roomName: json['room_name'] as String,
isAdmin: json['is_admin'] as bool, isAdmin: json['is_admin'] as bool,
participants: participants: (json['participants'] as List<dynamic>)
(json['participants'] as List<dynamic>)
.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>)) .map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
@@ -288,12 +269,10 @@ _SnRealtimeCall _$SnRealtimeCallFromJson(Map<String, dynamic> json) =>
id: json['id'] as String, id: json['id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
endedAt: endedAt: json['ended_at'] == null
json['ended_at'] == null
? null ? null
: DateTime.parse(json['ended_at'] as String), : DateTime.parse(json['ended_at'] as String),
senderId: json['sender_id'] as String, senderId: json['sender_id'] as String,

View File

@@ -12,28 +12,23 @@ _CustomApp _$CustomAppFromJson(Map<String, dynamic> json) => _CustomApp(
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
description: json['description'] as String?, description: json['description'] as String?,
status: (json['status'] as num?)?.toInt() ?? 0, status: (json['status'] as num?)?.toInt() ?? 0,
picture: picture: json['picture'] == null
json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background: background: json['background'] == null
json['background'] == null
? null ? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification: verification: json['verification'] == null
json['verification'] == null
? null ? null
: SnVerificationMark.fromJson( : SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>, json['verification'] as Map<String, dynamic>,
), ),
oauthConfig: oauthConfig: json['oauth_config'] == null
json['oauth_config'] == null
? null ? null
: CustomAppOauthConfig.fromJson( : CustomAppOauthConfig.fromJson(
json['oauth_config'] as Map<String, dynamic>, json['oauth_config'] as Map<String, dynamic>,
), ),
links: links: json['links'] == null
json['links'] == null
? null ? null
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>), : CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
secrets: secrets:
@@ -83,8 +78,7 @@ _CustomAppOauthConfig _$CustomAppOauthConfigFromJson(
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
const [], const [],
postLogoutRedirectUris: postLogoutRedirectUris: (json['post_logout_redirect_uris'] as List<dynamic>?)
(json['post_logout_redirect_uris'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList(), .toList(),
allowedScopes: allowedScopes:
@@ -118,8 +112,7 @@ _CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
id: json['id'] as String? ?? '', id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '', secret: json['secret'] as String? ?? '',
description: json['description'] as String?, description: json['description'] as String?,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
isOidc: json['is_oidc'] as bool? ?? false, isOidc: json['is_oidc'] as bool? ?? false,

View File

@@ -9,8 +9,7 @@ part of 'developer.dart';
_SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper( _SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper(
id: json['id'] as String, id: json['id'] as String,
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
publisher: publisher: json['publisher'] == null
json['publisher'] == null
? null ? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), : SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
); );

View File

@@ -22,8 +22,7 @@ _DriveTask _$DriveTaskFromJson(Map<String, dynamic> json) => _DriveTask(
transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(), transmissionProgress: (json['transmission_progress'] as num?)?.toDouble(),
errorMessage: json['error_message'] as String?, errorMessage: json['error_message'] as String?,
statusMessage: json['status_message'] as String?, statusMessage: json['status_message'] as String?,
result: result: json['result'] == null
json['result'] == null
? null ? null
: SnCloudFile.fromJson(json['result'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['result'] as Map<String, dynamic>),
poolId: json['pool_id'] as String?, poolId: json['pool_id'] as String?,

View File

@@ -17,8 +17,7 @@ _SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
siteName: json['site_name'] as String?, siteName: json['site_name'] as String?,
contentType: json['content_type'] as String?, contentType: json['content_type'] as String?,
author: json['author'] as String?, author: json['author'] as String?,
publishedDate: publishedDate: json['published_date'] == null
json['published_date'] == null
? null ? null
: DateTime.parse(json['published_date'] as String), : DateTime.parse(json['published_date'] as String),
); );

View File

@@ -35,8 +35,7 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
description: json['description'] as String?, description: json['description'] as String?,
fileMeta: json['file_meta'] as Map<String, dynamic>?, fileMeta: json['file_meta'] as Map<String, dynamic>?,
userMeta: json['user_meta'] as Map<String, dynamic>?, userMeta: json['user_meta'] as Map<String, dynamic>?,
pool: pool: json['pool'] == null
json['pool'] == null
? null ? null
: SnFilePool.fromJson(json['pool'] as Map<String, dynamic>), : SnFilePool.fromJson(json['pool'] as Map<String, dynamic>),
sensitiveMarks: sensitiveMarks:
@@ -47,15 +46,13 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
mimeType: json['mime_type'] as String?, mimeType: json['mime_type'] as String?,
hash: json['hash'] as String?, hash: json['hash'] as String?,
size: (json['size'] as num).toInt(), size: (json['size'] as num).toInt(),
uploadedAt: uploadedAt: json['uploaded_at'] == null
json['uploaded_at'] == null
? null ? null
: DateTime.parse(json['uploaded_at'] as String), : DateTime.parse(json['uploaded_at'] as String),
uploadedTo: json['uploaded_to'] as String?, uploadedTo: json['uploaded_to'] as String?,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -87,8 +84,7 @@ _SnCloudFileIndex _$SnCloudFileIndexFromJson(Map<String, dynamic> json) =>
file: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>), file: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -16,16 +16,13 @@ _SnFilePool _$SnFilePoolFromJson(Map<String, dynamic> json) => _SnFilePool(
isHidden: json['is_hidden'] as bool?, isHidden: json['is_hidden'] as bool?,
accountId: json['account_id'] as String?, accountId: json['account_id'] as String?,
resourceIdentifier: json['resource_identifier'] as String?, resourceIdentifier: json['resource_identifier'] as String?,
createdAt: createdAt: json['created_at'] == null
json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: updatedAt: json['updated_at'] == null
json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -10,8 +10,7 @@ _SnHeatmap _$SnHeatmapFromJson(Map<String, dynamic> json) => _SnHeatmap(
unit: json['unit'] as String, unit: json['unit'] as String,
periodStart: DateTime.parse(json['period_start'] as String), periodStart: DateTime.parse(json['period_start'] as String),
periodEnd: DateTime.parse(json['period_end'] as String), periodEnd: DateTime.parse(json['period_end'] as String),
items: items: (json['items'] as List<dynamic>)
(json['items'] as List<dynamic>)
.map((e) => SnHeatmapItem.fromJson(e as Map<String, dynamic>)) .map((e) => SnHeatmapItem.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );

View File

@@ -8,29 +8,23 @@ part of 'poll.dart';
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) => _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
_SnPollWithStats( _SnPollWithStats(
userAnswer: userAnswer: json['user_answer'] == null
json['user_answer'] == null
? null ? null
: SnPollAnswer.fromJson( : SnPollAnswer.fromJson(json['user_answer'] as Map<String, dynamic>),
json['user_answer'] as Map<String, dynamic>,
),
stats: json['stats'] as Map<String, dynamic>? ?? const {}, stats: json['stats'] as Map<String, dynamic>? ?? const {},
id: json['id'] as String, id: json['id'] as String,
questions: questions: (json['questions'] as List<dynamic>)
(json['questions'] as List<dynamic>)
.map((e) => SnPollQuestion.fromJson(e as Map<String, dynamic>)) .map((e) => SnPollQuestion.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
endedAt: endedAt: json['ended_at'] == null
json['ended_at'] == null
? null ? null
: DateTime.parse(json['ended_at'] as String), : DateTime.parse(json['ended_at'] as String),
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -52,25 +46,21 @@ Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
_SnPoll _$SnPollFromJson(Map<String, dynamic> json) => _SnPoll( _SnPoll _$SnPollFromJson(Map<String, dynamic> json) => _SnPoll(
id: json['id'] as String, id: json['id'] as String,
questions: questions: (json['questions'] as List<dynamic>)
(json['questions'] as List<dynamic>)
.map((e) => SnPollQuestion.fromJson(e as Map<String, dynamic>)) .map((e) => SnPollQuestion.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
endedAt: endedAt: json['ended_at'] == null
json['ended_at'] == null
? null ? null
: DateTime.parse(json['ended_at'] as String), : DateTime.parse(json['ended_at'] as String),
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
publisher: publisher: json['publisher'] == null
json['publisher'] == null
? null ? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), : SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -92,8 +82,7 @@ _SnPollQuestion _$SnPollQuestionFromJson(Map<String, dynamic> json) =>
_SnPollQuestion( _SnPollQuestion(
id: json['id'] as String, id: json['id'] as String,
type: $enumDecode(_$SnPollQuestionTypeEnumMap, json['type']), type: $enumDecode(_$SnPollQuestionTypeEnumMap, json['type']),
options: options: (json['options'] as List<dynamic>?)
(json['options'] as List<dynamic>?)
?.map((e) => SnPollOption.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnPollOption.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
title: json['title'] as String, title: json['title'] as String,
@@ -145,12 +134,10 @@ _SnPollAnswer _$SnPollAnswerFromJson(Map<String, dynamic> json) =>
pollId: json['poll_id'] as String, pollId: json['poll_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
); );

View File

@@ -11,12 +11,10 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
language: json['language'] as String?, language: json['language'] as String?,
editedAt: editedAt: json['edited_at'] == null
json['edited_at'] == null
? null ? null
: DateTime.parse(json['edited_at'] as String), : DateTime.parse(json['edited_at'] as String),
publishedAt: publishedAt: json['published_at'] == null
json['published_at'] == null
? null ? null
: DateTime.parse(json['published_at'] as String), : DateTime.parse(json['published_at'] as String),
visibility: (json['visibility'] as num?)?.toInt() ?? 0, visibility: (json['visibility'] as num?)?.toInt() ?? 0,
@@ -24,12 +22,9 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
slug: json['slug'] as String?, slug: json['slug'] as String?,
type: (json['type'] as num?)?.toInt() ?? 0, type: (json['type'] as num?)?.toInt() ?? 0,
meta: json['meta'] as Map<String, dynamic>?, meta: json['meta'] as Map<String, dynamic>?,
embedView: embedView: json['embed_view'] == null
json['embed_view'] == null
? null ? null
: SnPostEmbedView.fromJson( : SnPostEmbedView.fromJson(json['embed_view'] as Map<String, dynamic>),
json['embed_view'] as Map<String, dynamic>,
),
viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0, viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0,
viewsTotal: (json['views_total'] as num?)?.toInt() ?? 0, viewsTotal: (json['views_total'] as num?)?.toInt() ?? 0,
upvotes: (json['upvotes'] as num?)?.toInt() ?? 0, upvotes: (json['upvotes'] as num?)?.toInt() ?? 0,
@@ -38,23 +33,19 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
awardedScore: (json['awarded_score'] as num?)?.toInt() ?? 0, awardedScore: (json['awarded_score'] as num?)?.toInt() ?? 0,
pinMode: (json['pin_mode'] as num?)?.toInt(), pinMode: (json['pin_mode'] as num?)?.toInt(),
threadedPostId: json['threaded_post_id'] as String?, threadedPostId: json['threaded_post_id'] as String?,
threadedPost: threadedPost: json['threaded_post'] == null
json['threaded_post'] == null
? null ? null
: SnPost.fromJson(json['threaded_post'] as Map<String, dynamic>), : SnPost.fromJson(json['threaded_post'] as Map<String, dynamic>),
repliedPostId: json['replied_post_id'] as String?, repliedPostId: json['replied_post_id'] as String?,
repliedPost: repliedPost: json['replied_post'] == null
json['replied_post'] == null
? null ? null
: SnPost.fromJson(json['replied_post'] as Map<String, dynamic>), : SnPost.fromJson(json['replied_post'] as Map<String, dynamic>),
forwardedPostId: json['forwarded_post_id'] as String?, forwardedPostId: json['forwarded_post_id'] as String?,
forwardedPost: forwardedPost: json['forwarded_post'] == null
json['forwarded_post'] == null
? null ? null
: SnPost.fromJson(json['forwarded_post'] as Map<String, dynamic>), : SnPost.fromJson(json['forwarded_post'] as Map<String, dynamic>),
realmId: json['realm_id'] as String?, realmId: json['realm_id'] as String?,
realm: realm: json['realm'] == null
json['realm'] == null
? null ? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>), : SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
attachments: attachments:
@@ -90,16 +81,13 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
?.map((e) => SnPostFeaturedRecord.fromJson(e as Map<String, dynamic>)) ?.map((e) => SnPostFeaturedRecord.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
createdAt: createdAt: json['created_at'] == null
json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: updatedAt: json['updated_at'] == null
json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
repliedGone: json['replied_gone'] as bool? ?? false, repliedGone: json['replied_gone'] as bool? ?? false,
@@ -214,16 +202,13 @@ _SnPostAward _$SnPostAwardFromJson(Map<String, dynamic> json) => _SnPostAward(
message: json['message'] as String?, message: json['message'] as String?,
postId: json['post_id'] as String, postId: json['post_id'] as String,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: createdAt: json['created_at'] == null
json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: updatedAt: json['updated_at'] == null
json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -250,12 +235,10 @@ _SnPostReaction _$SnPostReactionFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -278,15 +261,13 @@ _SnPostFeaturedRecord _$SnPostFeaturedRecordFromJson(
) => _SnPostFeaturedRecord( ) => _SnPostFeaturedRecord(
id: json['id'] as String, id: json['id'] as String,
postId: json['post_id'] as String, postId: json['post_id'] as String,
featuredAt: featuredAt: json['featured_at'] == null
json['featured_at'] == null
? null ? null
: DateTime.parse(json['featured_at'] as String), : DateTime.parse(json['featured_at'] as String),
socialCredits: (json['social_credits'] as num).toInt(), socialCredits: (json['social_credits'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -17,8 +17,7 @@ _SnPublicationSite _$SnPublicationSiteFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
pages: pages: (json['pages'] as List<dynamic>)
(json['pages'] as List<dynamic>)
.map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>)) .map((e) => SnPublicationPage.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );

View File

@@ -12,34 +12,27 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
nick: json['nick'] as String? ?? '', nick: json['nick'] as String? ?? '',
bio: json['bio'] as String? ?? '', bio: json['bio'] as String? ?? '',
picture: picture: json['picture'] == null
json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background: background: json['background'] == null
json['background'] == null
? null ? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?, accountId: json['account_id'] as String?,
createdAt: createdAt: json['created_at'] == null
json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: updatedAt: json['updated_at'] == null
json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?, realmId: json['realm_id'] as String?,
verification: verification: json['verification'] == null
json['verification'] == null
? null ? null
: SnVerificationMark.fromJson( : SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>, json['verification'] as Map<String, dynamic>,
@@ -67,24 +60,20 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
_SnPublisherMember _$SnPublisherMemberFromJson(Map<String, dynamic> json) => _SnPublisherMember _$SnPublisherMemberFromJson(Map<String, dynamic> json) =>
_SnPublisherMember( _SnPublisherMember(
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
publisher: publisher: json['publisher'] == null
json['publisher'] == null
? null ? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), : SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
role: (json['role'] as num).toInt(), role: (json['role'] as num).toInt(),
joinedAt: joinedAt: json['joined_at'] == null
json['joined_at'] == null
? null ? null
: DateTime.parse(json['joined_at'] as String), : DateTime.parse(json['joined_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -12,25 +12,21 @@ _SnRealm _$SnRealmFromJson(Map<String, dynamic> json) => _SnRealm(
name: json['name'] as String? ?? '', name: json['name'] as String? ?? '',
description: json['description'] as String? ?? '', description: json['description'] as String? ?? '',
verifiedAs: json['verified_as'] as String?, verifiedAs: json['verified_as'] as String?,
verifiedAt: verifiedAt: json['verified_at'] == null
json['verified_at'] == null
? null ? null
: DateTime.parse(json['verified_at'] as String), : DateTime.parse(json['verified_at'] as String),
isCommunity: json['is_community'] as bool, isCommunity: json['is_community'] as bool,
isPublic: json['is_public'] as bool, isPublic: json['is_public'] as bool,
picture: picture: json['picture'] == null
json['picture'] == null
? null ? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background: background: json['background'] == null
json['background'] == null
? null ? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -55,32 +51,25 @@ Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
_SnRealmMember _$SnRealmMemberFromJson(Map<String, dynamic> json) => _SnRealmMember _$SnRealmMemberFromJson(Map<String, dynamic> json) =>
_SnRealmMember( _SnRealmMember(
realmId: json['realm_id'] as String, realmId: json['realm_id'] as String,
realm: realm: json['realm'] == null
json['realm'] == null
? null ? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>), : SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
role: (json['role'] as num).toInt(), role: (json['role'] as num).toInt(),
joinedAt: joinedAt: json['joined_at'] == null
json['joined_at'] == null
? null ? null
: DateTime.parse(json['joined_at'] as String), : DateTime.parse(json['joined_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
status: status: json['status'] == null
json['status'] == null
? null ? null
: SnAccountStatus.fromJson( : SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
json['status'] as Map<String, dynamic>,
),
); );
Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) => Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) =>

View File

@@ -9,20 +9,17 @@ part of 'reference.dart';
_Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference( _Reference _$ReferenceFromJson(Map<String, dynamic> json) => _Reference(
id: json['id'] as String, id: json['id'] as String,
fileId: json['file_id'] as String, fileId: json['file_id'] as String,
file: file: json['file'] == null
json['file'] == null
? null ? null
: SnCloudFile.fromJson(json['file'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['file'] as Map<String, dynamic>),
usage: json['usage'] as String, usage: json['usage'] as String,
resourceId: json['resource_id'] as String, resourceId: json['resource_id'] as String,
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -8,24 +8,20 @@ part of 'relationship.dart';
_SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) => _SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) =>
_SnRelationship( _SnRelationship(
createdAt: createdAt: json['created_at'] == null
json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: updatedAt: json['updated_at'] == null
json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>), account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
relatedId: json['related_id'] as String, relatedId: json['related_id'] as String,
related: SnAccount.fromJson(json['related'] as Map<String, dynamic>), related: SnAccount.fromJson(json['related'] as Map<String, dynamic>),
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
status: (json['status'] as num).toInt(), status: (json['status'] as num).toInt(),

View File

@@ -11,14 +11,12 @@ _SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker(
slug: json['slug'] as String, slug: json['slug'] as String,
image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>), image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>),
packId: json['pack_id'] as String, packId: json['pack_id'] as String,
pack: pack: json['pack'] == null
json['pack'] == null
? null ? null
: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>), : SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -42,18 +40,15 @@ _SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
description: json['description'] as String, description: json['description'] as String,
prefix: json['prefix'] as String, prefix: json['prefix'] as String,
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
icon: icon: json['icon'] == null
json['icon'] == null
? null ? null
: SnCloudFile.fromJson(json['icon'] as Map<String, dynamic>), : SnCloudFile.fromJson(json['icon'] as Map<String, dynamic>),
publisher: publisher: json['publisher'] == null
json['publisher'] == null
? null ? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), : SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
stickers: stickers:

View File

@@ -16,12 +16,10 @@ _StreamThinkingRequest _$StreamThinkingRequestFromJson(
?.map((e) => e as String) ?.map((e) => e as String)
.toList() ?? .toList() ??
const [], const [],
attachedPosts: attachedPosts: (json['attached_posts'] as List<dynamic>?)
(json['attached_posts'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList(), .toList(),
attachedMessages: attachedMessages: (json['attached_messages'] as List<dynamic>?)
(json['attached_messages'] as List<dynamic>?)
?.map((e) => e as Map<String, dynamic>) ?.map((e) => e as Map<String, dynamic>)
.toList(), .toList(),
serviceId: json['service_id'] as String?, serviceId: json['service_id'] as String?,
@@ -87,14 +85,10 @@ _SnThinkingMessagePart _$SnThinkingMessagePartFromJson(
(json['type'] as num).toInt(), (json['type'] as num).toInt(),
), ),
text: json['text'] as String?, text: json['text'] as String?,
functionCall: functionCall: json['function_call'] == null
json['function_call'] == null
? null ? null
: SnFunctionCall.fromJson( : SnFunctionCall.fromJson(json['function_call'] as Map<String, dynamic>),
json['function_call'] as Map<String, dynamic>, functionResult: json['function_result'] == null
),
functionResult:
json['function_result'] == null
? null ? null
: SnFunctionResult.fromJson( : SnFunctionResult.fromJson(
json['function_result'] as Map<String, dynamic>, json['function_result'] as Map<String, dynamic>,
@@ -119,8 +113,7 @@ _SnThinkingSequence _$SnThinkingSequenceFromJson(Map<String, dynamic> json) =>
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -159,16 +152,14 @@ _SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
tokenCount: (json['token_count'] as num?)?.toInt(), tokenCount: (json['token_count'] as num?)?.toInt(),
modelName: json['model_name'] as String?, modelName: json['model_name'] as String?,
sequenceId: json['sequence_id'] as String, sequenceId: json['sequence_id'] as String,
sequence: sequence: json['sequence'] == null
json['sequence'] == null
? null ? null
: SnThinkingSequence.fromJson( : SnThinkingSequence.fromJson(
json['sequence'] as Map<String, dynamic>, json['sequence'] as Map<String, dynamic>,
), ),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -206,8 +197,7 @@ _ThoughtServicesResponse _$ThoughtServicesResponseFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => _ThoughtServicesResponse( ) => _ThoughtServicesResponse(
defaultService: json['default_service'] as String, defaultService: json['default_service'] as String,
services: services: (json['services'] as List<dynamic>)
(json['services'] as List<dynamic>)
.map((e) => ThoughtService.fromJson(e as Map<String, dynamic>)) .map((e) => ThoughtService.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );

View File

@@ -8,19 +8,16 @@ part of 'wallet.dart';
_SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet( _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet(
id: json['id'] as String, id: json['id'] as String,
pockets: pockets: (json['pockets'] as List<dynamic>)
(json['pockets'] as List<dynamic>)
.map((e) => SnWalletPocket.fromJson(e as Map<String, dynamic>)) .map((e) => SnWalletPocket.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -77,8 +74,7 @@ _SnWalletPocket _$SnWalletPocketFromJson(Map<String, dynamic> json) =>
walletId: json['wallet_id'] as String, walletId: json['wallet_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -102,19 +98,16 @@ _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
remarks: json['remarks'] as String?, remarks: json['remarks'] as String?,
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
payerWalletId: json['payer_wallet_id'] as String?, payerWalletId: json['payer_wallet_id'] as String?,
payerWallet: payerWallet: json['payer_wallet'] == null
json['payer_wallet'] == null
? null ? null
: SnWallet.fromJson(json['payer_wallet'] as Map<String, dynamic>), : SnWallet.fromJson(json['payer_wallet'] as Map<String, dynamic>),
payeeWalletId: json['payee_wallet_id'] as String?, payeeWalletId: json['payee_wallet_id'] as String?,
payeeWallet: payeeWallet: json['payee_wallet'] == null
json['payee_wallet'] == null
? null ? null
: SnWallet.fromJson(json['payee_wallet'] as Map<String, dynamic>), : SnWallet.fromJson(json['payee_wallet'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -140,8 +133,7 @@ _SnWalletSubscription _$SnWalletSubscriptionFromJson(
) => _SnWalletSubscription( ) => _SnWalletSubscription(
id: json['id'] as String, id: json['id'] as String,
begunAt: DateTime.parse(json['begun_at'] as String), begunAt: DateTime.parse(json['begun_at'] as String),
endedAt: endedAt: json['ended_at'] == null
json['ended_at'] == null
? null ? null
: DateTime.parse(json['ended_at'] as String), : DateTime.parse(json['ended_at'] as String),
identifier: json['identifier'] as String, identifier: json['identifier'] as String,
@@ -153,21 +145,18 @@ _SnWalletSubscription _$SnWalletSubscriptionFromJson(
basePrice: (json['base_price'] as num?)?.toDouble(), basePrice: (json['base_price'] as num?)?.toDouble(),
couponId: json['coupon_id'] as String?, couponId: json['coupon_id'] as String?,
coupon: json['coupon'], coupon: json['coupon'],
renewalAt: renewalAt: json['renewal_at'] == null
json['renewal_at'] == null
? null ? null
: DateTime.parse(json['renewal_at'] as String), : DateTime.parse(json['renewal_at'] as String),
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
isAvailable: json['is_available'] as bool? ?? true, isAvailable: json['is_available'] as bool? ?? true,
finalPrice: (json['final_price'] as num?)?.toDouble(), finalPrice: (json['final_price'] as num?)?.toDouble(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -204,8 +193,7 @@ _SnWalletSubscriptionRef _$SnWalletSubscriptionRefFromJson(
isActive: json['is_active'] as bool, isActive: json['is_active'] as bool,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
@@ -239,8 +227,7 @@ _SnWalletOrder _$SnWalletOrderFromJson(Map<String, dynamic> json) =>
issuerAppId: json['issuer_app_id'] as String?, issuerAppId: json['issuer_app_id'] as String?,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -269,41 +256,34 @@ _SnWalletGift _$SnWalletGiftFromJson(Map<String, dynamic> json) =>
giftCode: json['gift_code'] as String, giftCode: json['gift_code'] as String,
subscriptionIdentifier: json['subscription_identifier'] as String, subscriptionIdentifier: json['subscription_identifier'] as String,
recipientId: json['recipient_id'] as String?, recipientId: json['recipient_id'] as String?,
recipient: recipient: json['recipient'] == null
json['recipient'] == null
? null ? null
: SnAccount.fromJson(json['recipient'] as Map<String, dynamic>), : SnAccount.fromJson(json['recipient'] as Map<String, dynamic>),
gifterId: json['gifter_id'] as String, gifterId: json['gifter_id'] as String,
gifter: gifter: json['gifter'] == null
json['gifter'] == null
? null ? null
: SnAccount.fromJson(json['gifter'] as Map<String, dynamic>), : SnAccount.fromJson(json['gifter'] as Map<String, dynamic>),
redeemerId: json['redeemer_id'] as String?, redeemerId: json['redeemer_id'] as String?,
redeemer: redeemer: json['redeemer'] == null
json['redeemer'] == null
? null ? null
: SnAccount.fromJson(json['redeemer'] as Map<String, dynamic>), : SnAccount.fromJson(json['redeemer'] as Map<String, dynamic>),
message: json['message'] as String?, message: json['message'] as String?,
status: (json['status'] as num).toInt(), status: (json['status'] as num).toInt(),
redeemedAt: redeemedAt: json['redeemed_at'] == null
json['redeemed_at'] == null
? null ? null
: DateTime.parse(json['redeemed_at'] as String), : DateTime.parse(json['redeemed_at'] as String),
expiredAt: expiredAt: json['expired_at'] == null
json['expired_at'] == null
? null ? null
: DateTime.parse(json['expired_at'] as String), : DateTime.parse(json['expired_at'] as String),
subscriptionId: json['subscription_id'] as String?, subscriptionId: json['subscription_id'] as String?,
subscription: subscription: json['subscription'] == null
json['subscription'] == null
? null ? null
: SnWalletSubscription.fromJson( : SnWalletSubscription.fromJson(
json['subscription'] as Map<String, dynamic>, json['subscription'] as Map<String, dynamic>,
), ),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -330,9 +310,8 @@ Map<String, dynamic> _$SnWalletGiftToJson(_SnWalletGift instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnWalletFund _$SnWalletFundFromJson( _SnWalletFund _$SnWalletFundFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> json, _SnWalletFund(
) => _SnWalletFund(
id: json['id'] as String, id: json['id'] as String,
currency: json['currency'] as String, currency: json['currency'] as String,
totalAmount: (json['total_amount'] as num).toDouble(), totalAmount: (json['total_amount'] as num).toDouble(),
@@ -342,23 +321,20 @@ _SnWalletFund _$SnWalletFundFromJson(
status: (json['status'] as num).toInt(), status: (json['status'] as num).toInt(),
message: json['message'] as String?, message: json['message'] as String?,
creatorAccountId: json['creator_account_id'] as String, creatorAccountId: json['creator_account_id'] as String,
creatorAccount: creatorAccount: json['creator_account'] == null
json['creator_account'] == null
? null ? null
: SnAccount.fromJson(json['creator_account'] as Map<String, dynamic>), : SnAccount.fromJson(json['creator_account'] as Map<String, dynamic>),
expiredAt: DateTime.parse(json['expired_at'] as String), expiredAt: DateTime.parse(json['expired_at'] as String),
recipients: recipients: (json['recipients'] as List<dynamic>)
(json['recipients'] as List<dynamic>)
.map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>)) .map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
isOpen: json['is_open'] as bool, isOpen: json['is_open'] as bool,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
Map<String, dynamic> _$SnWalletFundToJson(_SnWalletFund instance) => Map<String, dynamic> _$SnWalletFundToJson(_SnWalletFund instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -386,22 +362,17 @@ _SnWalletFundRecipient _$SnWalletFundRecipientFromJson(
id: json['id'] as String, id: json['id'] as String,
fundId: json['fund_id'] as String, fundId: json['fund_id'] as String,
recipientAccountId: json['recipient_account_id'] as String, recipientAccountId: json['recipient_account_id'] as String,
recipientAccount: recipientAccount: json['recipient_account'] == null
json['recipient_account'] == null
? null ? null
: SnAccount.fromJson( : SnAccount.fromJson(json['recipient_account'] as Map<String, dynamic>),
json['recipient_account'] as Map<String, dynamic>,
),
amount: (json['amount'] as num).toDouble(), amount: (json['amount'] as num).toDouble(),
isReceived: json['is_received'] as bool, isReceived: json['is_received'] as bool,
receivedAt: receivedAt: json['received_at'] == null
json['received_at'] == null
? null ? null
: DateTime.parse(json['received_at'] as String), : DateTime.parse(json['received_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -425,31 +396,27 @@ _SnLotteryTicket _$SnLotteryTicketFromJson(Map<String, dynamic> json) =>
_SnLotteryTicket( _SnLotteryTicket(
id: json['id'] as String, id: json['id'] as String,
accountId: json['account_id'] as String, accountId: json['account_id'] as String,
account: account: json['account'] == null
json['account'] == null
? null ? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>), : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
regionOneNumbers: regionOneNumbers: (json['region_one_numbers'] as List<dynamic>)
(json['region_one_numbers'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
regionTwoNumber: (json['region_two_number'] as num).toInt(), regionTwoNumber: (json['region_two_number'] as num).toInt(),
multiplier: (json['multiplier'] as num).toInt(), multiplier: (json['multiplier'] as num).toInt(),
drawStatus: (json['draw_status'] as num).toInt(), drawStatus: (json['draw_status'] as num).toInt(),
drawDate: drawDate: json['draw_date'] == null
json['draw_date'] == null
? null ? null
: DateTime.parse(json['draw_date'] as String), : DateTime.parse(json['draw_date'] as String),
matchedRegionOneNumbers: matchedRegionOneNumbers:
(json['matched_region_one_numbers'] as List<dynamic>?) (json['matched_region_one_numbers'] as List<dynamic>?)
?.map((e) => (e as num).toInt()) ?.map((e) => (e as num).toInt())
.toList(), .toList(),
matchedRegionTwoNumber: matchedRegionTwoNumber: (json['matched_region_two_number'] as num?)
(json['matched_region_two_number'] as num?)?.toInt(), ?.toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -471,26 +438,24 @@ Map<String, dynamic> _$SnLotteryTicketToJson(_SnLotteryTicket instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnLotteryRecord _$SnLotteryRecordFromJson(Map<String, dynamic> json) => _SnLotteryRecord _$SnLotteryRecordFromJson(
_SnLotteryRecord( Map<String, dynamic> json,
) => _SnLotteryRecord(
id: json['id'] as String, id: json['id'] as String,
drawDate: DateTime.parse(json['draw_date'] as String), drawDate: DateTime.parse(json['draw_date'] as String),
winningRegionOneNumbers: winningRegionOneNumbers: (json['winning_region_one_numbers'] as List<dynamic>)
(json['winning_region_one_numbers'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
winningRegionTwoNumber: winningRegionTwoNumber: (json['winning_region_two_number'] as num).toInt(),
(json['winning_region_two_number'] as num).toInt(),
totalTickets: (json['total_tickets'] as num).toInt(), totalTickets: (json['total_tickets'] as num).toInt(),
totalPrizesAwarded: (json['total_prizes_awarded'] as num).toInt(), totalPrizesAwarded: (json['total_prizes_awarded'] as num).toInt(),
totalPrizeAmount: (json['total_prize_amount'] as num).toDouble(), totalPrizeAmount: (json['total_prize_amount'] as num).toDouble(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
Map<String, dynamic> _$SnLotteryRecordToJson(_SnLotteryRecord instance) => Map<String, dynamic> _$SnLotteryRecordToJson(_SnLotteryRecord instance) =>
<String, dynamic>{ <String, dynamic>{

View File

@@ -17,12 +17,10 @@ _SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed(
url: json['url'] as String, url: json['url'] as String,
title: json['title'] as String, title: json['title'] as String,
description: json['description'] as String?, description: json['description'] as String?,
preview: preview: json['preview'] == null
json['preview'] == null
? null ? null
: SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>), : SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
config: config: json['config'] == null
json['config'] == null
? const SnWebFeedConfig() ? const SnWebFeedConfig()
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>), : SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
publisherId: json['publisher_id'] as String, publisherId: json['publisher_id'] as String,
@@ -33,8 +31,7 @@ _SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed(
const [], const [],
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );
@@ -61,26 +58,20 @@ _SnWebArticle _$SnWebArticleFromJson(Map<String, dynamic> json) =>
url: json['url'] as String, url: json['url'] as String,
author: json['author'] as String?, author: json['author'] as String?,
meta: json['meta'] as Map<String, dynamic>?, meta: json['meta'] as Map<String, dynamic>?,
preview: preview: json['preview'] == null
json['preview'] == null
? null ? null
: SnScrappedLink.fromJson( : SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
json['preview'] as Map<String, dynamic>, feed: json['feed'] == null
),
feed:
json['feed'] == null
? null ? null
: SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>), : SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>),
content: json['content'] as String?, content: json['content'] as String?,
publishedAt: publishedAt: json['published_at'] == null
json['published_at'] == null
? null ? null
: DateTime.parse(json['published_at'] as String), : DateTime.parse(json['published_at'] as String),
feedId: json['feed_id'] as String, feedId: json['feed_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: deletedAt: json['deleted_at'] == null
json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
); );

View File

@@ -1 +0,0 @@

View File

@@ -14,7 +14,7 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
return response.data; return response.data;
} }
final indexedCloudFileListProvider = AsyncNotifierProvider( final indexedCloudFileListProvider = AsyncNotifierProvider.autoDispose(
IndexedCloudFileListNotifier.new, IndexedCloudFileListNotifier.new,
); );
@@ -76,10 +76,10 @@ class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
queryParameters: queryParameters, queryParameters: queryParameters,
); );
final List<String> folders = final List<String> folders = (response.data['folders'] as List)
(response.data['folders'] as List).map((e) => e as String).toList(); .map((e) => e as String)
final List<SnCloudFileIndex> files = .toList();
(response.data['files'] as List) final List<SnCloudFileIndex> files = (response.data['files'] as List)
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>)) .map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
.toList(); .toList();
@@ -92,7 +92,7 @@ class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
} }
} }
final unindexedFileListProvider = AsyncNotifierProvider( final unindexedFileListProvider = AsyncNotifierProvider.autoDispose(
UnindexedFileListNotifier.new, UnindexedFileListNotifier.new,
); );
@@ -165,13 +165,13 @@ class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0; totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
final List<SnCloudFile> files = final List<SnCloudFile> files = (response.data as List)
(response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) .map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList(); .toList();
final List<FileListItem> items = final List<FileListItem> items = files
files.map((file) => FileListItem.unindexedFile(file)).toList(); .map((file) => FileListItem.unindexedFile(file))
.toList();
return items; return items;
} }

View File

@@ -7,6 +7,7 @@ abstract class PaginationController<T> {
int get fetchedCount; int get fetchedCount;
bool get fetchedAll; bool get fetchedAll;
bool get isLoading;
FutureOr<List<T>> fetch(); FutureOr<List<T>> fetch();
@@ -32,11 +33,15 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
@override @override
bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!; bool get fetchedAll => totalCount != null && fetchedCount >= totalCount!;
@override
bool isLoading = false;
@override @override
FutureOr<List<T>> build() async => fetch(); FutureOr<List<T>> build() async => fetch();
@override @override
Future<void> refresh() async { Future<void> refresh() async {
isLoading = true;
totalCount = null; totalCount = null;
state = AsyncData<List<T>>([]); state = AsyncData<List<T>>([]);
@@ -44,12 +49,14 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
return await fetch(); return await fetch();
}); });
state = newState; state = newState;
isLoading = false;
} }
@override @override
Future<void> fetchFurther() async { Future<void> fetchFurther() async {
if (fetchedAll) return; if (fetchedAll) return;
isLoading = true;
state = AsyncLoading<List<T>>(); state = AsyncLoading<List<T>>();
final newState = await AsyncValue.guard<List<T>>(() async { final newState = await AsyncValue.guard<List<T>>(() async {
@@ -58,6 +65,7 @@ mixin AsyncPaginationController<T> on AsyncNotifier<List<T>>
}); });
state = newState; state = newState;
isLoading = false;
} }
} }
@@ -67,6 +75,7 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
Future<void> applyFilter(F filter) async { Future<void> applyFilter(F filter) async {
if (currentFilter == filter) return; if (currentFilter == filter) return;
// Reset the data // Reset the data
isLoading = true;
totalCount = null; totalCount = null;
state = AsyncData<List<T>>([]); state = AsyncData<List<T>>([]);
currentFilter = filter; currentFilter = filter;
@@ -75,5 +84,6 @@ mixin AsyncPaginationFilter<F, T> on AsyncPaginationController<T>
return await fetch(); return await fetch();
}); });
state = newState; state = newState;
isLoading = false;
} }
} }

View File

@@ -0,0 +1,52 @@
// Post Categories Notifier
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
final postCategoriesProvider =
AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
List<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
with AsyncPaginationController<SnPostCategory> {
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostCategory.fromJson(json)).toList();
}
}
// Post Tags Notifier
final postTagsProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
with AsyncPaginationController<SnPostTag> {
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostTag.fromJson(json)).toList();
}
}

View File

@@ -0,0 +1,95 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
part 'post_list.freezed.dart';
@freezed
sealed class PostListQuery with _$PostListQuery {
const factory PostListQuery({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
@Default(false) bool shuffle,
bool? includeReplies,
bool? mediaOnly,
String? queryTerm,
String? order,
int? periodStart,
int? periodEnd,
@Default(true) bool orderDesc,
}) = _PostListQuery;
}
@freezed
sealed class PostListQueryConfig with _$PostListQueryConfig {
const factory PostListQueryConfig({
String? id,
@Default(PostListQuery()) PostListQuery initialFilter,
}) = _PostListQueryConfig;
}
final postListProvider = AsyncNotifierProvider.autoDispose.family(
PostListNotifier.new,
);
class PostListNotifier extends AsyncNotifier<List<SnPost>>
with
AsyncPaginationController<SnPost>,
AsyncPaginationFilter<PostListQuery, SnPost> {
static const int pageSize = 20;
final String? id;
final PostListQueryConfig config;
PostListNotifier(this.config) : id = config.id;
@override
late PostListQuery currentFilter;
@override
Future<List<SnPost>> build() async {
currentFilter = config.initialFilter;
return fetch();
}
@override
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': currentFilter.includeReplies,
'orderDesc': currentFilter.orderDesc,
if (currentFilter.shuffle) 'shuffle': currentFilter.shuffle,
if (currentFilter.pubName != null) 'pub': currentFilter.pubName,
if (currentFilter.realm != null) 'realm': currentFilter.realm,
if (currentFilter.type != null) 'type': currentFilter.type,
if (currentFilter.tags != null) 'tags': currentFilter.tags,
if (currentFilter.categories != null)
'categories': currentFilter.categories,
if (currentFilter.pinned != null) 'pinned': currentFilter.pinned,
if (currentFilter.order != null) 'order': currentFilter.order,
if (currentFilter.periodStart != null)
'periodStart': currentFilter.periodStart,
if (currentFilter.periodEnd != null) 'periodEnd': currentFilter.periodEnd,
if (currentFilter.queryTerm != null) 'query': currentFilter.queryTerm,
if (currentFilter.mediaOnly != null) 'media': currentFilter.mediaOnly,
};
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
return response.data
.map((json) => SnPost.fromJson(json))
.cast<SnPost>()
.toList();
}
}

View File

@@ -317,4 +317,276 @@ as bool,
} }
/// @nodoc
mixin _$PostListQueryConfig {
String? get id; PostListQuery get initialFilter;
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PostListQueryConfigCopyWith<PostListQueryConfig> get copyWith => _$PostListQueryConfigCopyWithImpl<PostListQueryConfig>(this as PostListQueryConfig, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostListQueryConfig&&(identical(other.id, id) || other.id == id)&&(identical(other.initialFilter, initialFilter) || other.initialFilter == initialFilter));
}
@override
int get hashCode => Object.hash(runtimeType,id,initialFilter);
@override
String toString() {
return 'PostListQueryConfig(id: $id, initialFilter: $initialFilter)';
}
}
/// @nodoc
abstract mixin class $PostListQueryConfigCopyWith<$Res> {
factory $PostListQueryConfigCopyWith(PostListQueryConfig value, $Res Function(PostListQueryConfig) _then) = _$PostListQueryConfigCopyWithImpl;
@useResult
$Res call({
String? id, PostListQuery initialFilter
});
$PostListQueryCopyWith<$Res> get initialFilter;
}
/// @nodoc
class _$PostListQueryConfigCopyWithImpl<$Res>
implements $PostListQueryConfigCopyWith<$Res> {
_$PostListQueryConfigCopyWithImpl(this._self, this._then);
final PostListQueryConfig _self;
final $Res Function(PostListQueryConfig) _then;
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? initialFilter = null,}) {
return _then(_self.copyWith(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,initialFilter: null == initialFilter ? _self.initialFilter : initialFilter // ignore: cast_nullable_to_non_nullable
as PostListQuery,
));
}
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PostListQueryCopyWith<$Res> get initialFilter {
return $PostListQueryCopyWith<$Res>(_self.initialFilter, (value) {
return _then(_self.copyWith(initialFilter: value));
});
}
}
/// Adds pattern-matching-related methods to [PostListQueryConfig].
extension PostListQueryConfigPatterns on PostListQueryConfig {
/// 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( _PostListQueryConfig value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PostListQueryConfig() 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( _PostListQueryConfig value) $default,){
final _that = this;
switch (_that) {
case _PostListQueryConfig():
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( _PostListQueryConfig value)? $default,){
final _that = this;
switch (_that) {
case _PostListQueryConfig() 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, PostListQuery initialFilter)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PostListQueryConfig() when $default != null:
return $default(_that.id,_that.initialFilter);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, PostListQuery initialFilter) $default,) {final _that = this;
switch (_that) {
case _PostListQueryConfig():
return $default(_that.id,_that.initialFilter);}
}
/// 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, PostListQuery initialFilter)? $default,) {final _that = this;
switch (_that) {
case _PostListQueryConfig() when $default != null:
return $default(_that.id,_that.initialFilter);case _:
return null;
}
}
}
/// @nodoc
class _PostListQueryConfig implements PostListQueryConfig {
const _PostListQueryConfig({this.id, this.initialFilter = const PostListQuery()});
@override final String? id;
@override@JsonKey() final PostListQuery initialFilter;
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PostListQueryConfigCopyWith<_PostListQueryConfig> get copyWith => __$PostListQueryConfigCopyWithImpl<_PostListQueryConfig>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostListQueryConfig&&(identical(other.id, id) || other.id == id)&&(identical(other.initialFilter, initialFilter) || other.initialFilter == initialFilter));
}
@override
int get hashCode => Object.hash(runtimeType,id,initialFilter);
@override
String toString() {
return 'PostListQueryConfig(id: $id, initialFilter: $initialFilter)';
}
}
/// @nodoc
abstract mixin class _$PostListQueryConfigCopyWith<$Res> implements $PostListQueryConfigCopyWith<$Res> {
factory _$PostListQueryConfigCopyWith(_PostListQueryConfig value, $Res Function(_PostListQueryConfig) _then) = __$PostListQueryConfigCopyWithImpl;
@override @useResult
$Res call({
String? id, PostListQuery initialFilter
});
@override $PostListQueryCopyWith<$Res> get initialFilter;
}
/// @nodoc
class __$PostListQueryConfigCopyWithImpl<$Res>
implements _$PostListQueryConfigCopyWith<$Res> {
__$PostListQueryConfigCopyWithImpl(this._self, this._then);
final _PostListQueryConfig _self;
final $Res Function(_PostListQueryConfig) _then;
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? initialFilter = null,}) {
return _then(_PostListQueryConfig(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,initialFilter: null == initialFilter ? _self.initialFilter : initialFilter // ignore: cast_nullable_to_non_nullable
as PostListQuery,
));
}
/// Create a copy of PostListQueryConfig
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$PostListQueryCopyWith<$Res> get initialFilter {
return $PostListQueryCopyWith<$Res>(_self.initialFilter, (value) {
return _then(_self.copyWith(initialFilter: value));
});
}
}
// dart format on // dart format on

View File

@@ -1,13 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart'; import 'package:island/models/activity.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart'; import 'package:island/pods/paging.dart';
final activityListProvider = final activityListProvider = AsyncNotifierProvider.autoDispose(
AsyncNotifierProvider<ActivityListNotifier, List<SnTimelineEvent>>(
ActivityListNotifier.new, ActivityListNotifier.new,
); );
class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>> class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
with with
@@ -28,8 +26,6 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
if (cursor != null) 'cursor': cursor, if (cursor != null) 'cursor': cursor,
'take': pageSize, 'take': pageSize,
if (currentFilter != null) 'filter': currentFilter, if (currentFilter != null) 'filter': currentFilter,
if (kDebugMode)
'debugInclude': 'realms,publishers,articles,shuffledPosts',
}; };
final response = await client.get( final response = await client.get(
@@ -37,8 +33,7 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
queryParameters: queryParameters, queryParameters: queryParameters,
); );
final List<SnTimelineEvent> items = final List<SnTimelineEvent> items = (response.data as List)
(response.data as List)
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>)) .map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
.toList(); .toList();

View File

@@ -5,16 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/webfeed.dart'; import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>(( final webFeedListProvider = FutureProvider.autoDispose
ref, .family<List<SnWebFeed>, String>((ref, pubName) async {
pubName,
) async {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final response = await client.get('/sphere/publishers/$pubName/feeds'); final response = await client.get('/sphere/publishers/$pubName/feeds');
return (response.data as List) return (response.data as List)
.map((json) => SnWebFeed.fromJson(json)) .map((json) => SnWebFeed.fromJson(json))
.toList(); .toList();
}); });
class WebFeedNotifier extends AsyncNotifier<SnWebFeed> { class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
final ({String pubName, String? feedId}) arg; final ({String pubName, String? feedId}) arg;
@@ -51,8 +49,7 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final url = '/sphere/publishers/${feed.publisherId}/feeds'; final url = '/sphere/publishers/${feed.publisherId}/feeds';
final response = final response = feed.id.isEmpty
feed.id.isEmpty
? await client.post(url, data: feed.toJson()) ? await client.post(url, data: feed.toJson())
: await client.patch('$url/${feed.id}', data: feed.toJson()); : await client.patch('$url/${feed.id}', data: feed.toJson());

View File

@@ -105,8 +105,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'articleCompose', name: 'articleCompose',
path: '/articles/compose', path: '/articles/compose',
builder: builder: (context, state) => ArticleComposeScreen(
(context, state) => ArticleComposeScreen(
initialState: state.extra as PostComposeInitialState?, initialState: state.extra as PostComposeInitialState?,
), ),
), ),
@@ -190,8 +189,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'explore', name: 'explore',
path: '/', path: '/',
pageBuilder: pageBuilder: (context, state) => CustomTransitionPage(
(context, state) => CustomTransitionPage(
key: const ValueKey('explore'), key: const ValueKey('explore'),
child: const ExploreScreen(), child: const ExploreScreen(),
transitionsBuilder: _tabPagesTransitionBuilder, transitionsBuilder: _tabPagesTransitionBuilder,
@@ -220,11 +218,6 @@ final routerProvider = Provider<GoRouter>((ref) {
return PostCategoryDetailScreen(slug: slug, isCategory: true); return PostCategoryDetailScreen(slug: slug, isCategory: true);
}, },
), ),
GoRoute(
name: 'postTags',
path: '/posts/tags',
builder: (context, state) => const PostTagsListScreen(),
),
GoRoute( GoRoute(
name: 'postTagDetail', name: 'postTagDetail',
path: '/posts/tags/:slug', path: '/posts/tags/:slug',
@@ -260,8 +253,7 @@ final routerProvider = Provider<GoRouter>((ref) {
// Chat tab // Chat tab
ShellRoute( ShellRoute(
pageBuilder: pageBuilder: (context, state, child) => CustomTransitionPage(
(context, state, child) => CustomTransitionPage(
key: const ValueKey('chat'), key: const ValueKey('chat'),
child: ChatShellScreen(child: child), child: ChatShellScreen(child: child),
transitionsBuilder: _tabPagesTransitionBuilder, transitionsBuilder: _tabPagesTransitionBuilder,
@@ -303,8 +295,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'realmList', name: 'realmList',
path: '/realms', path: '/realms',
pageBuilder: pageBuilder: (context, state) => CustomTransitionPage(
(context, state) => CustomTransitionPage(
key: const ValueKey('realms'), key: const ValueKey('realms'),
child: const RealmListScreen(), child: const RealmListScreen(),
transitionsBuilder: _tabPagesTransitionBuilder, transitionsBuilder: _tabPagesTransitionBuilder,
@@ -336,8 +327,7 @@ final routerProvider = Provider<GoRouter>((ref) {
// Account tab // Account tab
ShellRoute( ShellRoute(
pageBuilder: pageBuilder: (context, state, child) => CustomTransitionPage(
(context, state, child) => CustomTransitionPage(
key: const ValueKey('account'), key: const ValueKey('account'),
child: AccountShellScreen(child: child), child: AccountShellScreen(child: child),
transitionsBuilder: _tabPagesTransitionBuilder, transitionsBuilder: _tabPagesTransitionBuilder,
@@ -352,8 +342,8 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'stickerMarketplace', name: 'stickerMarketplace',
path: '/stickers', path: '/stickers',
builder: builder: (context, state) =>
(context, state) => const MarketplaceStickersScreen(), const MarketplaceStickersScreen(),
routes: [ routes: [
GoRoute( GoRoute(
name: 'stickerPackDetail', name: 'stickerPackDetail',
@@ -368,8 +358,8 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'webFeedMarketplace', name: 'webFeedMarketplace',
path: '/feeds', path: '/feeds',
builder: builder: (context, state) =>
(context, state) => const MarketplaceWebFeedsScreen(), const MarketplaceWebFeedsScreen(),
routes: [ routes: [
GoRoute( GoRoute(
name: 'webFeedDetail', name: 'webFeedDetail',
@@ -516,26 +506,22 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'developerHub', name: 'developerHub',
path: '/developers', path: '/developers',
builder: builder: (context, state) => DeveloperHubScreen(
(context, state) => DeveloperHubScreen( initialPublisherName: state.uri.queryParameters['publisher'],
initialPublisherName:
state.uri.queryParameters['publisher'],
initialProjectId: state.uri.queryParameters['project'], initialProjectId: state.uri.queryParameters['project'],
), ),
routes: [ routes: [
GoRoute( GoRoute(
name: 'developerProjectNew', name: 'developerProjectNew',
path: ':name/projects/new', path: ':name/projects/new',
builder: builder: (context, state) => NewProjectScreen(
(context, state) => NewProjectScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
), ),
), ),
GoRoute( GoRoute(
name: 'developerProjectEdit', name: 'developerProjectEdit',
path: ':name/projects/:id/edit', path: ':name/projects/:id/edit',
builder: builder: (context, state) => EditProjectScreen(
(context, state) => EditProjectScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!, id: state.pathParameters['id']!,
), ),
@@ -558,8 +544,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'developerAppDetail', name: 'developerAppDetail',
path: 'apps/:appId', path: 'apps/:appId',
builder: builder: (context, state) => AppDetailScreen(
(context, state) => AppDetailScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!, projectId: state.pathParameters['projectId']!,
appId: state.pathParameters['appId']!, appId: state.pathParameters['appId']!,
@@ -568,8 +553,7 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
name: 'developerBotDetail', name: 'developerBotDetail',
path: 'bots/:botId', path: 'bots/:botId',
builder: builder: (context, state) => BotDetailScreen(
(context, state) => BotDetailScreen(
publisherName: state.pathParameters['name']!, publisherName: state.pathParameters['name']!,
projectId: state.pathParameters['projectId']!, projectId: state.pathParameters['projectId']!,
botId: state.pathParameters['botId']!, botId: state.pathParameters['botId']!,

View File

@@ -331,8 +331,7 @@ class AccountScreen extends HookConsumerWidget {
if (availableWidth > totalMin) { if (availableWidth > totalMin) {
return Row( return Row(
spacing: 8, spacing: 8,
children: children: children
children
.map((child) => Expanded(child: child)) .map((child) => Expanded(child: child))
.toList(), .toList(),
).padding(horizontal: 12).height(48); ).padding(horizontal: 12).height(48);
@@ -341,8 +340,7 @@ class AccountScreen extends HookConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
spacing: 8, spacing: 8,
children: children: children
children
.map( .map(
(child) => (child) =>
SizedBox(width: minWidth, child: child), SizedBox(width: minWidth, child: child),
@@ -495,8 +493,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar(title: const Text('account').tr()), appBar: AppBar(title: const Text('account').tr()),
body: body: ConstrainedBox(
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360), constraints: const BoxConstraints(maxWidth: 360),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@@ -23,7 +23,7 @@ Future<double> socialCredits(Ref ref) async {
return response.data?.toDouble() ?? 0.0; return response.data?.toDouble() ?? 0.0;
} }
final socialCreditHistoryNotifierProvider = AsyncNotifierProvider( final socialCreditHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
SocialCreditHistoryNotifier.new, SocialCreditHistoryNotifier.new,
); );
@@ -45,8 +45,7 @@ class SocialCreditHistoryNotifier
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final records = final records = response.data
response.data
.map((json) => SnSocialCreditRecord.fromJson(json)) .map((json) => SnSocialCreditRecord.fromJson(json))
.cast<SnSocialCreditRecord>() .cast<SnSocialCreditRecord>()
.toList(); .toList();
@@ -68,8 +67,7 @@ class SocialCreditsTab extends HookConsumerWidget {
margin: const EdgeInsets.only(left: 16, right: 16, top: 8), margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
child: socialCredits child: socialCredits
.when( .when(
data: data: (credits) => Stack(
(credits) => Stack(
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -83,9 +81,7 @@ class SocialCreditsTab extends HookConsumerWidget {
? 'socialCreditsLevelGood'.tr() ? 'socialCreditsLevelGood'.tr()
: 'socialCreditsLevelExcellent'.tr(), : 'socialCreditsLevelExcellent'.tr(),
).tr().bold().fontSize(20), ).tr().bold().fontSize(20),
Text( Text('${credits.toStringAsFixed(2)} pts').fontSize(14),
'${credits.toStringAsFixed(2)} pts',
).fontSize(14),
const Gap(8), const Gap(8),
LinearProgressIndicator(value: credits / 200), LinearProgressIndicator(value: credits / 200),
], ],
@@ -119,8 +115,7 @@ class SocialCreditsTab extends HookConsumerWidget {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text( title: Text(
record.reason, record.reason,
style: style: isExpired
isExpired
? TextStyle( ? TextStyle(
decoration: TextDecoration.lineThrough, decoration: TextDecoration.lineThrough,
color: Theme.of( color: Theme.of(

View File

@@ -14,7 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
final levelingHistoryNotifierProvider = AsyncNotifierProvider( final levelingHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
LevelingHistoryNotifier.new, LevelingHistoryNotifier.new,
); );
@@ -35,8 +35,7 @@ class LevelingHistoryNotifier extends AsyncNotifier<List<SnExperienceRecord>>
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<SnExperienceRecord> records = final List<SnExperienceRecord> records = response.data
response.data
.map((json) => SnExperienceRecord.fromJson(json)) .map((json) => SnExperienceRecord.fromJson(json))
.cast<SnExperienceRecord>() .cast<SnExperienceRecord>()
.toList(); .toList();
@@ -162,8 +161,9 @@ class LevelingScreen extends HookConsumerWidget {
stopIndicatorRadius: 0, stopIndicatorRadius: 0,
trackGap: 0, trackGap: 0,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.surfaceContainerHigh, context,
).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
), ),
], ],
@@ -186,8 +186,7 @@ class LevelingScreen extends HookConsumerWidget {
notifier: levelingHistoryNotifierProvider.notifier, notifier: levelingHistoryNotifierProvider.notifier,
isRefreshable: false, isRefreshable: false,
isSliver: true, isSliver: true,
itemBuilder: itemBuilder: (context, idx, record) => ListTile(
(context, idx, record) => ListTile(
title: Column( title: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -208,9 +207,7 @@ class LevelingScreen extends HookConsumerWidget {
subtitle: Row( subtitle: Row(
spacing: 8, spacing: 8,
children: [ children: [
Text( Text('${record.delta > 0 ? '+' : ''}${record.delta} EXP'),
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
),
if (record.bonusMultiplier != 1.0) if (record.bonusMultiplier != 1.0)
Text('x${record.bonusMultiplier}'), Text('x${record.bonusMultiplier}'),
], ],
@@ -249,8 +246,7 @@ class LevelStairsPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final paint = final paint = Paint()
Paint()
..color = surfaceColor.withOpacity(0.2) ..color = surfaceColor.withOpacity(0.2)
..strokeWidth = 1.5 ..strokeWidth = 1.5
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;

View File

@@ -29,7 +29,7 @@ Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
.toList(); .toList();
} }
final relationshipListNotifierProvider = AsyncNotifierProvider( final relationshipListNotifierProvider = AsyncNotifierProvider.autoDispose(
RelationshipListNotifier.new, RelationshipListNotifier.new,
); );
@@ -45,8 +45,7 @@ class RelationshipListNotifier extends AsyncNotifier<List<SnRelationship>>
queryParameters: {'offset': fetchedCount.toString(), 'take': take}, queryParameters: {'offset': fetchedCount.toString(), 'take': take},
); );
final List<SnRelationship> items = final List<SnRelationship> items = (response.data as List)
(response.data as List)
.map((e) => SnRelationship.fromJson(e as Map<String, dynamic>)) .map((e) => SnRelationship.fromJson(e as Map<String, dynamic>))
.cast<SnRelationship>() .cast<SnRelationship>()
.toList(); .toList();
@@ -83,8 +82,9 @@ class RelationshipListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final account = final account = showRelatedAccount
showRelatedAccount ? relationship.related : relationship.account; ? relationship.related
: relationship.account;
final isPending = final isPending =
relationship.status == 0 && relationship.relatedId == currentUserId; relationship.status == 0 && relationship.relatedId == currentUserId;
final isWaiting = final isWaiting =
@@ -138,8 +138,7 @@ class RelationshipListTile extends StatelessWidget {
], ],
), ),
subtitle: Text('@${account.name}'), subtitle: Text('@${account.name}'),
trailing: trailing: showActions
showActions
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -165,8 +164,7 @@ class RelationshipListTile extends StatelessWidget {
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: const Icon(Symbols.more_vert), icon: const Icon(Symbols.more_vert),
itemBuilder: itemBuilder: (context) => [
(context) => [
if (relationship.status >= 100) // If friend if (relationship.status >= 100) // If friend
PopupMenuItem( PopupMenuItem(
child: ListTile( child: ListTile(
@@ -174,11 +172,7 @@ class RelationshipListTile extends StatelessWidget {
title: Text('blockUser').tr(), title: Text('blockUser').tr(),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
onTap: onTap: () => onUpdateStatus?.call(relationship, -100),
() => onUpdateStatus?.call(
relationship,
-100,
),
) )
else if (relationship.status <= -100) // If blocked else if (relationship.status <= -100) // If blocked
PopupMenuItem( PopupMenuItem(
@@ -187,9 +181,7 @@ class RelationshipListTile extends StatelessWidget {
title: Text('unblockUser').tr(), title: Text('unblockUser').tr(),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
onTap: onTap: () => onUpdateStatus?.call(relationship, 100),
() =>
onUpdateStatus?.call(relationship, 100),
), ),
], ],
), ),
@@ -299,6 +291,7 @@ class RelationshipScreen extends HookConsumerWidget {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: PaginationList( child: PaginationList(
padding: EdgeInsets.zero,
provider: relationshipListNotifierProvider, provider: relationshipListNotifierProvider,
notifier: relationshipListNotifierProvider.notifier, notifier: relationshipListNotifierProvider.notifier,
itemBuilder: (context, index, relationship) { itemBuilder: (context, index, relationship) {
@@ -380,9 +373,7 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: requests.when( child: requests.when(
data: data: (items) => items.isEmpty
(items) =>
items.isEmpty
? Center( ? Center(
child: Text( child: Text(
'friendSentRequestEmpty'.tr(), 'friendSentRequestEmpty'.tr(),

View File

@@ -20,6 +20,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:island/pods/chat/chat_room.dart'; import 'package:island/pods/chat/chat_room.dart';
@@ -50,8 +51,7 @@ class ChatRoomListTile extends HookConsumerWidget {
if (validMembers.isNotEmpty) { if (validMembers.isNotEmpty) {
final userInfo = ref.watch(userInfoProvider); final userInfo = ref.watch(userInfoProvider);
if (userInfo.value != null) { if (userInfo.value != null) {
validMembers = validMembers = validMembers
validMembers
.where((e) => e.accountId != userInfo.value!.id) .where((e) => e.accountId != userInfo.value!.id)
.toList(); .toList();
} }
@@ -60,37 +60,60 @@ class ChatRoomListTile extends HookConsumerWidget {
Widget buildSubtitle() { Widget buildSubtitle() {
if (subtitle != null) return subtitle!; if (subtitle != null) return subtitle!;
return summary.when( return AnimatedSwitcher(
data: (data) { duration: const Duration(milliseconds: 300),
if (data == null) { layoutBuilder: (currentChild, previousChildren) => Stack(
return isDirect && room.description == null alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: summary.when(
data: (data) => Container(
key: const ValueKey('data'),
child: data == null
? isDirect && room.description == null
? Text( ? Text(
validMembers.map((e) => '@${e.account.name}').join(', '), validMembers
.map((e) => '@${e.account.name}')
.join(', '),
maxLines: 1, maxLines: 1,
) )
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1); : Text(
} room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
return Column( )
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (data.unreadCount > 0) if (data.unreadCount > 0)
Text( Text(
'unreadMessages'.plural(data.unreadCount), 'unreadMessages'.plural(data.unreadCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
), ),
if (data.lastMessage == null) if (data.lastMessage == null)
Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1) Text(
room.description ?? 'descriptionNone'.tr(),
maxLines: 1,
)
else else
Row( Row(
spacing: 4, spacing: 4,
children: [ children: [
Badge( Badge(
label: Text(data.lastMessage!.sender.account.nick), label: Text(
textColor: Theme.of(context).colorScheme.onPrimary, data.lastMessage!.sender.account.nick,
backgroundColor: Theme.of(context).colorScheme.primary, ),
textColor: Theme.of(
context,
).colorScheme.onPrimary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
), ),
Expanded( Expanded(
child: Text( child: Text(
@@ -114,19 +137,37 @@ class ChatRoomListTile extends HookConsumerWidget {
], ],
), ),
], ],
),
),
loading: () => Container(
key: const ValueKey('loading'),
child: Builder(
builder: (context) {
final seed = DateTime.now().microsecondsSinceEpoch;
final len = 4 + (seed % 17); // 4..20 inclusive
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var s = seed;
final buffer = StringBuffer();
for (var i = 0; i < len; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
buffer.write(chars[s % chars.length]);
}
return Skeletonizer(
enabled: true,
child: Text(buffer.toString()),
); );
}, },
loading: () => const SizedBox.shrink(), ),
error: ),
(_, _) => error: (_, _) => Container(
isDirect && room.description == null key: const ValueKey('error'),
child: isDirect && room.description == null
? Text( ? Text(
validMembers.map((e) => '@${e.account.name}').join(', '), validMembers.map((e) => '@${e.account.name}').join(', '),
maxLines: 1, maxLines: 1,
) )
: Text( : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
room.description ?? 'descriptionNone'.tr(), ),
maxLines: 1,
), ),
); );
} }
@@ -149,11 +190,9 @@ class ChatRoomListTile extends HookConsumerWidget {
loading: () => false, loading: () => false,
error: (_, _) => false, error: (_, _) => false,
), ),
child: child: (isDirect && room.picture?.id == null)
(isDirect && room.picture?.id == null)
? SplitAvatarWidget( ? SplitAvatarWidget(
filesId: filesId: validMembers
validMembers
.map((e) => e.account.profile.picture?.id) .map((e) => e.account.profile.picture?.id)
.toList(), .toList(),
) )
@@ -199,8 +238,7 @@ class ChatListBodyWidget extends HookConsumerWidget {
builder: (context, ref, _) { builder: (context, ref, _) {
final summaryState = ref.watch(chatSummaryProvider); final summaryState = ref.watch(chatSummaryProvider);
return summaryState.maybeWhen( return summaryState.maybeWhen(
loading: loading: () => const LinearProgressIndicator(
() => const LinearProgressIndicator(
minHeight: 2, minHeight: 2,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
), ),
@@ -210,16 +248,13 @@ class ChatListBodyWidget extends HookConsumerWidget {
), ),
Expanded( Expanded(
child: chats.when( child: chats.when(
data: data: (items) => RefreshIndicator(
(items) => RefreshIndicator( onRefresh: () => Future.sync(() {
onRefresh:
() => Future.sync(() {
ref.invalidate(chatRoomJoinedProvider); ref.invalidate(chatRoomJoinedProvider);
}), }),
child: SuperListView.builder( child: SuperListView.builder(
padding: EdgeInsets.only(bottom: 96), padding: EdgeInsets.only(bottom: 96),
itemCount: itemCount: items
items
.where( .where(
(item) => (item) =>
selectedTab.value == 0 || selectedTab.value == 0 ||
@@ -228,13 +263,11 @@ class ChatListBodyWidget extends HookConsumerWidget {
) )
.length, .length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final filteredItems = final filteredItems = items
items
.where( .where(
(item) => (item) =>
selectedTab.value == 0 || selectedTab.value == 0 ||
(selectedTab.value == 1 && (selectedTab.value == 1 && item.type == 1) ||
item.type == 1) ||
(selectedTab.value == 2 && item.type != 1), (selectedTab.value == 2 && item.type != 1),
) )
.toList(); .toList();
@@ -260,8 +293,7 @@ class ChatListBodyWidget extends HookConsumerWidget {
), ),
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (error, stack) => ResponseErrorWidget(
(error, stack) => ResponseErrorWidget(
error: error, error: error,
onRetry: () { onRetry: () {
ref.invalidate(chatRoomJoinedProvider); ref.invalidate(chatRoomJoinedProvider);
@@ -552,15 +584,9 @@ class _ChatInvitesSheet extends HookConsumerWidget {
), ),
], ],
child: invites.when( child: invites.when(
data: data: (items) => items.isEmpty
(items) =>
items.isEmpty
? Center( ? Center(
child: child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@@ -576,10 +602,10 @@ class _ChatInvitesSheet extends HookConsumerWidget {
if (invite.chatRoom!.type == 1) if (invite.chatRoom!.type == 1)
Badge( Badge(
label: const Text('directMessage').tr(), label: const Text('directMessage').tr(),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
textColor: ).colorScheme.primary,
Theme.of(context).colorScheme.onPrimary, textColor: Theme.of(context).colorScheme.onPrimary,
), ),
], ],
), ),

View File

@@ -98,8 +98,7 @@ class PublisherMemberListNotifier extends AsyncNotifier<List<SnPublisherMember>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final members = final members = response.data
response.data
.map((e) => SnPublisherMember.fromJson(e)) .map((e) => SnPublisherMember.fromJson(e))
.cast<SnPublisherMember>() .cast<SnPublisherMember>()
.toList(); .toList();
@@ -173,12 +172,10 @@ class PublisherSelector extends StatelessWidget {
iconStyleData: IconStyleData( iconStyleData: IconStyleData(
icon: Icon(Icons.arrow_drop_down), icon: Icon(Icons.arrow_drop_down),
iconSize: 19, iconSize: 19,
iconEnabledColor: iconEnabledColor: isWideScreen(context)
isWideScreen(context)
? null ? null
: Theme.of(context).appBarTheme.foregroundColor!, : Theme.of(context).appBarTheme.foregroundColor!,
iconDisabledColor: iconDisabledColor: isWideScreen(context)
isWideScreen(context)
? null ? null
: Theme.of(context).appBarTheme.foregroundColor!, : Theme.of(context).appBarTheme.foregroundColor!,
), ),
@@ -204,6 +201,13 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
if (!hasPublishers) ...[ if (!hasPublishers) ...[
if (publishers.isLoading)
Padding(
padding: const EdgeInsets.all(8),
child: const CircularProgressIndicator(),
)
else
...([
const Icon( const Icon(
Symbols.info, Symbols.info,
fill: 1, fill: 1,
@@ -214,6 +218,7 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
).tr(), ).tr(),
]),
const Gap(24), const Gap(24),
], ],
if (hasPublishers) if (hasPublishers)
@@ -288,14 +293,14 @@ class CreatorHubScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) =>
(context) =>
EditPublisherScreen(name: currentPublisher.value!.name), EditPublisherScreen(name: currentPublisher.value!.name),
).then((value) async { ).then((value) async {
if (value == null) return; if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future); final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value = currentPublisher.value = data
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull; .where((e) => e.id == currentPublisher.value!.id)
.firstOrNull;
}); });
} }
@@ -315,9 +320,7 @@ class CreatorHubScreen extends HookConsumerWidget {
} }
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when( final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
data: data: (data) => data
(data) =>
data
.map( .map(
(item) => DropdownMenuItem<SnPublisher>( (item) => DropdownMenuItem<SnPublisher>(
value: item, value: item,
@@ -329,8 +332,7 @@ class CreatorHubScreen extends HookConsumerWidget {
), ),
title: Text(item.nick), title: Text(item.nick),
subtitle: Text('@${item.name}'), subtitle: Text('@${item.name}'),
trailing: trailing: currentPublisher.value?.id == item.id
currentPublisher.value?.id == item.id
? const Icon(Icons.check) ? const Icon(Icons.check)
: null, : null,
contentPadding: EdgeInsets.symmetric(horizontal: 8), contentPadding: EdgeInsets.symmetric(horizontal: 8),
@@ -443,8 +445,7 @@ class CreatorHubScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder: (context) => _PublisherMemberListSheet(
(context) => _PublisherMemberListSheet(
publisherUname: currentPublisher.value!.name, publisherUname: currentPublisher.value!.name,
), ),
); );
@@ -567,11 +568,9 @@ class CreatorHubScreen extends HookConsumerWidget {
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
child: publisherStats.when( child: publisherStats.when(
data: data: (stats) => SingleChildScrollView(
(stats) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 24), padding: const EdgeInsets.symmetric(vertical: 24),
child: child: currentPublisher.value == null
currentPublisher.value == null
? ConstrainedBox( ? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: _PublisherUnselectedWidget( child: _PublisherUnselectedWidget(
@@ -876,8 +875,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder: (context) => _PublisherMemberRoleSheet(
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname, publisherUname: publisherUname,
member: member, member: member,
), ),
@@ -991,12 +989,8 @@ class _PublisherMemberRoleSheet extends HookConsumerWidget {
onSelected: (int selection) { onSelected: (int selection) {
roleController.text = selection.toString(); roleController.text = selection.toString();
}, },
fieldViewBuilder: ( fieldViewBuilder:
context, (context, controller, focusNode, onFieldSubmitted) {
controller,
focusNode,
onFieldSubmitted,
) {
return TextField( return TextField(
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
@@ -1085,15 +1079,9 @@ class _PublisherInviteSheet extends HookConsumerWidget {
), ),
], ],
child: invites.when( child: invites.when(
data: data: (items) => items.isEmpty
(items) =>
items.isEmpty
? Center( ? Center(
child: child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@@ -1106,8 +1094,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
fallbackIcon: Symbols.group, fallbackIcon: Symbols.group,
), ),
title: Text(invite.publisher!.nick), title: Text(invite.publisher!.nick),
subtitle: subtitle: Text(
Text(
invite.role >= 100 invite.role >= 100
? 'permissionOwner' ? 'permissionOwner'
: invite.role >= 50 : invite.role >= 50
@@ -1131,8 +1118,7 @@ class _PublisherInviteSheet extends HookConsumerWidget {
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (error, _) => ResponseErrorWidget(
(error, _) => ResponseErrorWidget(
error: error, error: error,
onRetry: () => ref.invalidate(publisherInvitesProvider), onRetry: () => ref.invalidate(publisherInvitesProvider),
), ),

View File

@@ -44,8 +44,7 @@ class PollListNotifier extends AsyncNotifier<List<SnPollWithStats>>
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final items = final items = response.data
response.data
.map((json) => SnPollWithStats.fromJson(json)) .map((json) => SnPollWithStats.fromJson(json))
.cast<SnPollWithStats>() .cast<SnPollWithStats>()
.toList(); .toList();
@@ -91,6 +90,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
body: ExtendedRefreshIndicator( body: ExtendedRefreshIndicator(
onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future), onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future),
child: PaginationList( child: PaginationList(
footerSkeletonMaxWidth: 640,
provider: pollListNotifierProvider(pubName), provider: pollListNotifierProvider(pubName),
notifier: pollListNotifierProvider(pubName).notifier, notifier: pollListNotifierProvider(pubName).notifier,
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
@@ -119,8 +119,7 @@ class _CreatorPollItem extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final ended = pollWithStats.endedAt; final ended = pollWithStats.endedAt;
final endedText = final endedText = ended == null
ended == null
? 'No end' ? 'No end'
: MaterialLocalizations.of(context).formatFullDate(ended); : MaterialLocalizations.of(context).formatFullDate(ended);
@@ -152,8 +151,7 @@ class _CreatorPollItem extends HookConsumerWidget {
], ],
), ),
trailing: PopupMenuButton<String>( trailing: PopupMenuButton<String>(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -167,8 +165,7 @@ class _CreatorPollItem extends HookConsumerWidget {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
isDismissible: false, isDismissible: false,
builder: builder: (context) => PollEditorScreen(
(context) => PollEditorScreen(
initialPublisher: pubName, initialPublisher: pubName,
initialPollId: pollWithStats.id, initialPollId: pollWithStats.id,
), ),
@@ -189,21 +186,16 @@ class _CreatorPollItem extends HookConsumerWidget {
onTap: () async { onTap: () async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('Delete Poll'), title: Text('Delete Poll'),
content: Text( content: Text('Are you sure you want to delete this poll?'),
'Are you sure you want to delete this poll?',
),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(false),
() => Navigator.of(context).pop(false),
child: Text('Cancel'), child: Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: onPressed: () => Navigator.of(context).pop(true),
() => Navigator.of(context).pop(true),
child: Text('Delete'), child: Text('Delete'),
), ),
], ],
@@ -212,9 +204,7 @@ class _CreatorPollItem extends HookConsumerWidget {
if (confirmed == true) { if (confirmed == true) {
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.delete( await client.delete('/sphere/polls/${pollWithStats.id}');
'/sphere/polls/${pollWithStats.id}',
);
ref.invalidate(pollListNotifierProvider(pubName)); ref.invalidate(pollListNotifierProvider(pubName));
showSnackBar('Poll deleted successfully'); showSnackBar('Poll deleted successfully');
} catch (e) { } catch (e) {

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_list.dart'; import 'package:island/widgets/post/post_list.dart';
@@ -20,7 +21,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
key: ValueKey(refreshKey.value), key: ValueKey(refreshKey.value),
slivers: [ slivers: [
SliverPostList( SliverPostList(
pubName: pubName, query: PostListQuery(pubName: pubName),
itemType: PostItemType.creator, itemType: PostItemType.creator,
maxWidth: 640, maxWidth: 640,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer, backgroundColor: Theme.of(context).colorScheme.surfaceContainer,

View File

@@ -12,7 +12,6 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose( final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
@@ -38,8 +37,7 @@ class SiteListNotifier extends AsyncNotifier<List<SnPublicationSite>>
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final items = final items = response.data
response.data
.map((json) => SnPublicationSite.fromJson(json)) .map((json) => SnPublicationSite.fromJson(json))
.cast<SnPublicationSite>() .cast<SnPublicationSite>()
.toList(); .toList();
@@ -70,14 +68,11 @@ class CreatorSiteListScreen extends HookConsumerWidget {
onPressed: () => _createSite(context), onPressed: () => _createSite(context),
child: Icon(Icons.add), child: Icon(Icons.add),
), ),
body: ExtendedRefreshIndicator( body: PaginationList(
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future), footerSkeletonMaxWidth: 640,
child: CustomScrollView(
slivers: [
const SliverGap(8),
PaginationList(
provider: siteListNotifierProvider(pubName), provider: siteListNotifierProvider(pubName),
notifier: siteListNotifierProvider(pubName).notifier, notifier: siteListNotifierProvider(pubName).notifier,
padding: const EdgeInsets.only(top: 12),
itemBuilder: (context, index, site) { itemBuilder: (context, index, site) {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
@@ -85,9 +80,6 @@ class CreatorSiteListScreen extends HookConsumerWidget {
).center(); ).center();
}, },
), ),
],
),
),
); );
} }
} }
@@ -148,8 +140,7 @@ class _CreatorSiteItem extends HookConsumerWidget {
), ),
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -162,11 +153,8 @@ class _CreatorSiteItem extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) =>
(context) => SiteForm( SiteForm(pubName: pubName, siteSlug: site.slug),
pubName: pubName,
siteSlug: site.slug,
),
); );
}, },
), ),
@@ -179,26 +167,10 @@ class _CreatorSiteItem extends HookConsumerWidget {
], ],
), ),
onTap: () async { onTap: () async {
final confirmed = await showDialog<bool>( final confirmed = await showConfirmAlert(
context: context, 'publicationSiteDeleteConfirm'.tr(),
builder: 'deleteSite'.tr(),
(context) => AlertDialog( isDanger: true,
title: Text('deleteSite'.tr()),
content: Text('deleteSiteConfirm'.tr()),
actions: [
TextButton(
onPressed:
() =>
Navigator.of(context).pop(false),
child: Text('cancel'.tr()),
),
TextButton(
onPressed:
() => Navigator.of(context).pop(true),
child: Text('delete'.tr()),
),
],
),
); );
if (confirmed == true) { if (confirmed == true) {
try { try {

View File

@@ -41,8 +41,7 @@ class StickersScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createStickerPack'.tr(), titleText: 'createStickerPack'.tr(),
child: StickerPackForm(pubName: pubName), child: StickerPackForm(pubName: pubName),
), ),
@@ -54,8 +53,7 @@ class StickersScreen extends HookConsumerWidget {
}, },
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
), ),
body: body: isWideScreen(context)
isWideScreen(context)
? Center( ? Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
@@ -83,6 +81,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return PaginationList( return PaginationList(
padding: EdgeInsets.zero,
provider: stickerPacksProvider(pubName), provider: stickerPacksProvider(pubName),
notifier: stickerPacksProvider(pubName).notifier, notifier: stickerPacksProvider(pubName).notifier,
itemBuilder: (context, index, sticker) { itemBuilder: (context, index, sticker) {
@@ -97,8 +96,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: sticker.name, titleText: sticker.name,
actions: [ actions: [
IconButton( IconButton(
@@ -108,8 +106,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createSticker'.tr(), titleText: 'createSticker'.tr(),
child: StickerForm(packId: id), child: StickerForm(packId: id),
), ),
@@ -165,8 +162,7 @@ class StickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final stickers = final stickers = response.data
response.data
.map((e) => SnStickerPack.fromJson(e)) .map((e) => SnStickerPack.fromJson(e))
.cast<SnStickerPack>() .cast<SnStickerPack>()
.toList(); .toList();
@@ -262,8 +258,7 @@ class StickerPackForm extends HookConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: child: (icon.value?.isEmpty ?? true)
(icon.value?.isEmpty ?? true)
? const SizedBox.shrink() ? const SizedBox.shrink()
: CloudImageWidget(fileId: icon.value!), : CloudImageWidget(fileId: icon.value!),
), ),
@@ -273,8 +268,7 @@ class StickerPackForm extends HookConsumerWidget {
onPressed: () { onPressed: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: builder: (context) => CloudFilePicker(
(context) => CloudFilePicker(
allowedTypes: {UniversalFileType.image}, allowedTypes: {UniversalFileType.image},
), ),
).then((value) { ).then((value) {
@@ -300,8 +294,8 @@ class StickerPackForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
@@ -314,8 +308,8 @@ class StickerPackForm extends HookConsumerWidget {
), ),
minLines: 3, minLines: 3,
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: prefixController, controller: prefixController,
@@ -332,8 +326,8 @@ class StickerPackForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),

View File

@@ -39,8 +39,7 @@ class AppDetailScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: onPressed: appData.value == null
appData.value == null
? null ? null
: () { : () {
context.pushNamed( context.pushNamed(
@@ -85,21 +84,31 @@ class AppDetailScreen extends HookConsumerWidget {
controller: tabController, controller: tabController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
_AppOverview(app: app), Align(
AppSecretsScreen( alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: _AppOverview(app: app),
),
),
Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: AppSecretsScreen(
publisherName: publisherName, publisherName: publisherName,
projectId: projectId, projectId: projectId,
appId: appId, appId: appId,
), ),
),
),
], ],
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () => ref.invalidate(
() => ref.invalidate(
customAppProvider(publisherName, projectId, appId), customAppProvider(publisherName, projectId, appId),
), ),
), ),
@@ -115,6 +124,7 @@ class _AppOverview extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
AspectRatio( AspectRatio(
@@ -125,8 +135,7 @@ class _AppOverview extends StatelessWidget {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: child: app.background != null
app.background != null
? CloudFileWidget( ? CloudFileWidget(
item: app.background!, item: app.background!,
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app_secret.dart'; import 'package:island/models/custom_app_secret.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
@@ -53,8 +54,7 @@ class AppSecretsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'newSecretGenerated'.tr(), titleText: 'newSecretGenerated'.tr(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@@ -114,23 +114,39 @@ class AppSecretsScreen extends HookConsumerWidget {
controller: descriptionController, controller: descriptionController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'description'.tr(), labelText: 'description'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
),
), ),
autofocus: true, autofocus: true,
), ),
const SizedBox(height: 20), const Gap(16),
TextFormField( TextFormField(
controller: expiresInController, controller: expiresInController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'expiresIn'.tr(), labelText: 'expiresIn'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
const SizedBox(height: 20), const Gap(16),
SwitchListTile( Card(
margin: EdgeInsets.zero,
child: SwitchListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
title: Text('isOidc'.tr()), title: Text('isOidc'.tr()),
value: isOidc.value, value: isOidc.value,
onChanged: (value) => isOidc.value = value, onChanged: (value) => isOidc.value = value,
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
FilledButton.icon( FilledButton.icon(
onPressed: () async { onPressed: () async {
@@ -175,13 +191,8 @@ class AppSecretsScreen extends HookConsumerWidget {
return secrets.when( return secrets.when(
data: (data) { data: (data) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: onRefresh: () => ref.refresh(
() => ref.refresh( customAppSecretsProvider(publisherName, projectId, appId).future,
customAppSecretsProvider(
publisherName,
projectId,
appId,
).future,
), ),
child: Column( child: Column(
children: [ children: [
@@ -240,11 +251,9 @@ class AppSecretsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () => ref.invalidate(
() => ref.invalidate(
customAppSecretsProvider(publisherName, projectId, appId), customAppSecretsProvider(publisherName, projectId, appId),
), ),
), ),

View File

@@ -76,8 +76,7 @@ class CustomAppsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createCustomApp'.tr(), titleText: 'createCustomApp'.tr(),
child: NewCustomAppScreen( child: NewCustomAppScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -95,10 +94,8 @@ class CustomAppsScreen extends HookConsumerWidget {
); );
} }
return ExtendedRefreshIndicator( return ExtendedRefreshIndicator(
onRefresh: onRefresh: () =>
() => ref.refresh( ref.refresh(customAppsProvider(publisherName, projectId).future),
customAppsProvider(publisherName, projectId).future,
),
child: Column( child: Column(
children: [ children: [
const Gap(8), const Gap(8),
@@ -110,8 +107,7 @@ class CustomAppsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createCustomApp'.tr(), titleText: 'createCustomApp'.tr(),
child: NewCustomAppScreen( child: NewCustomAppScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -145,32 +141,21 @@ class CustomAppsScreen extends HookConsumerWidget {
); );
}, },
child: Column( child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [ children: [
if (app.background != null) if (app.background != null)
CloudFileWidget( AspectRatio(
aspectRatio: 16 / 7,
child: CloudFileWidget(
item: app.background!, item: app.background!,
fit: BoxFit.cover, fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8), ).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
), ),
ListTile( ListTile(
title: Text(app.name), title: Text(app.name),
leading: ProfilePictureWidget(
fileId: app.picture?.id,
fallbackIcon: Symbols.apps,
),
subtitle: Text( subtitle: Text(
app.slug, app.slug,
style: GoogleFonts.robotoMono(fontSize: 12), style: GoogleFonts.robotoMono(fontSize: 12),
@@ -180,8 +165,7 @@ class CustomAppsScreen extends HookConsumerWidget {
right: 12, right: 12,
), ),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
value: 'edit', value: 'edit',
child: Row( child: Row(
@@ -203,9 +187,7 @@ class CustomAppsScreen extends HookConsumerWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'delete', 'delete',
style: TextStyle( style: TextStyle(color: Colors.red),
color: Colors.red,
),
).tr(), ).tr(),
], ],
), ),
@@ -216,8 +198,7 @@ class CustomAppsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'editCustomApp'.tr(), titleText: 'editCustomApp'.tr(),
child: EditAppScreen( child: EditAppScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -264,13 +245,10 @@ class CustomAppsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () =>
() => ref.invalidate( ref.invalidate(customAppsProvider(publisherName, projectId)),
customAppsProvider(publisherName, projectId),
),
), ),
); );
} }

View File

@@ -36,8 +36,7 @@ class BotDetailScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.edit), icon: const Icon(Symbols.edit),
onPressed: onPressed: botData.value == null
botData.value == null
? null ? null
: () { : () {
context.pushNamed( context.pushNamed(
@@ -84,23 +83,32 @@ class BotDetailScreen extends HookConsumerWidget {
controller: tabController, controller: tabController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
_BotOverview(bot: bot), Align(
BotKeysScreen( alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: _BotOverview(bot: bot),
),
),
Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: BotKeysScreen(
publisherName: publisherName, publisherName: publisherName,
projectId: projectId, projectId: projectId,
botId: botId, botId: botId,
), ),
),
),
], ],
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () =>
() => ref.invalidate( ref.invalidate(botProvider(publisherName, projectId, botId)),
botProvider(publisherName, projectId, botId),
),
), ),
), ),
); );
@@ -124,8 +132,7 @@ class _BotOverview extends StatelessWidget {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: child: bot.account.profile.background != null
bot.account.profile.background != null
? CloudFileWidget( ? CloudFileWidget(
item: bot.account.profile.background!, item: bot.account.profile.background!,
fit: BoxFit.cover, fit: BoxFit.cover,

View File

@@ -53,8 +53,7 @@ class BotKeysScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'newKeyGenerated'.tr(), titleText: 'newKeyGenerated'.tr(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@@ -94,8 +93,8 @@ class BotKeysScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold( heightFactor: 0.7,
titleText: 'newBotKey'.tr(), titleText: 'newBotKey'.tr(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@@ -105,7 +104,12 @@ class BotKeysScreen extends HookConsumerWidget {
children: [ children: [
TextFormField( TextFormField(
controller: keyNameController, controller: keyNameController,
decoration: InputDecoration(labelText: 'keyName'.tr()), decoration: InputDecoration(
labelText: 'keyName'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
autofocus: true, autofocus: true,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -189,22 +193,15 @@ class BotKeysScreen extends HookConsumerWidget {
ListTile( ListTile(
leading: const Icon(Symbols.add), leading: const Icon(Symbols.add),
title: Text('newBotKey'.tr()), title: Text('newBotKey'.tr()),
trailing: const Icon(Symbols.chevron_right),
onTap: createKey, onTap: createKey,
), ),
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: child: data.isEmpty
data.isEmpty
? Center(child: Text('noBotKeys'.tr())) ? Center(child: Text('noBotKeys'.tr()))
: RefreshIndicator( : RefreshIndicator(
onRefresh: onRefresh: () => ref.refresh(
() => ref.refresh( botKeysProvider(publisherName, projectId, botId).future,
botKeysProvider(
publisherName,
projectId,
botId,
).future,
), ),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@@ -219,8 +216,7 @@ class BotKeysScreen extends HookConsumerWidget {
right: 12, right: 12,
), ),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
value: 'rotate', value: 'rotate',
child: Row( child: Row(
@@ -242,9 +238,7 @@ class BotKeysScreen extends HookConsumerWidget {
const Gap(12), const Gap(12),
Text( Text(
'revoke'.tr(), 'revoke'.tr(),
style: TextStyle( style: TextStyle(color: Colors.red),
color: Colors.red,
),
), ),
], ],
), ),
@@ -267,13 +261,10 @@ class BotKeysScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () =>
() => ref.invalidate( ref.invalidate(botKeysProvider(publisherName, projectId, botId)),
botKeysProvider(publisherName, projectId, botId),
),
), ),
); );
} }

View File

@@ -54,8 +54,7 @@ class BotsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createBot'.tr(), titleText: 'createBot'.tr(),
child: NewBotScreen( child: NewBotScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -73,8 +72,8 @@ class BotsScreen extends HookConsumerWidget {
); );
} }
return ExtendedRefreshIndicator( return ExtendedRefreshIndicator(
onRefresh: onRefresh: () =>
() => ref.refresh(botsProvider(publisherName, projectId).future), ref.refresh(botsProvider(publisherName, projectId).future),
child: Column( child: Column(
children: [ children: [
const Gap(8), const Gap(8),
@@ -86,8 +85,7 @@ class BotsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createBot'.tr(), titleText: 'createBot'.tr(),
child: NewBotScreen( child: NewBotScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -108,23 +106,30 @@ class BotsScreen extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final bot = data[index]; final bot = data[index];
return Card( return Card(
child: ListTile( child: Column(
shape: const RoundedRectangleBorder( children: [
borderRadius: BorderRadius.all(Radius.circular(8.0)), if (bot.account.profile.background != null)
AspectRatio(
aspectRatio: 16 / 7,
child: CloudFileWidget(
item: bot.account.profile.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
), ),
leading: CircleAvatar( ListTile(
child: shape: const RoundedRectangleBorder(
bot.account.profile.picture != null borderRadius: BorderRadius.all(
? ProfilePictureWidget( Radius.circular(8.0),
file: bot.account.profile.picture!, ),
) ),
: const Icon(Symbols.smart_toy), leading: ProfilePictureWidget(
fallbackIcon: Symbols.smart_toy,
file: bot.account.profile.picture,
), ),
title: Text(bot.account.nick), title: Text(bot.account.nick),
subtitle: Text(bot.account.name), subtitle: Text(bot.account.name),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
value: 'edit', value: 'edit',
child: Row( child: Row(
@@ -157,8 +162,7 @@ class BotsScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'editBot'.tr(), titleText: 'editBot'.tr(),
child: EditBotScreen( child: EditBotScreen(
publisherName: publisherName, publisherName: publisherName,
@@ -175,7 +179,9 @@ class BotsScreen extends HookConsumerWidget {
isDanger: true, isDanger: true,
).then((confirm) { ).then((confirm) {
if (confirm) { if (confirm) {
final client = ref.read(apiClientProvider); final client = ref.read(
apiClientProvider,
);
client.delete( client.delete(
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}', '/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
); );
@@ -198,6 +204,8 @@ class BotsScreen extends HookConsumerWidget {
); );
}, },
), ),
],
),
); );
}, },
), ),
@@ -207,11 +215,9 @@ class BotsScreen extends HookConsumerWidget {
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: onRetry: () => ref.invalidate(botsProvider(publisherName, projectId)),
() => ref.invalidate(botsProvider(publisherName, projectId)),
), ),
); );
} }

View File

@@ -68,8 +68,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
developers.value?.firstOrNull, developers.value?.firstOrNull,
); );
final projects = final projects = currentDeveloper.value?.publisher?.name != null
currentDeveloper.value?.publisher?.name != null
? ref.watch( ? ref.watch(
devProjectsProvider(currentDeveloper.value!.publisher!.name), devProjectsProvider(currentDeveloper.value!.publisher!.name),
) )
@@ -126,8 +125,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'createProject'.tr(), titleText: 'createProject'.tr(),
child: ProjectForm( child: ProjectForm(
publisherName: publisherName:
@@ -211,9 +209,7 @@ class _MainContentSection extends HookConsumerWidget {
return Container( return Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: developerStats.when( child: developerStats.when(
data: data: (stats) => currentDeveloper == null
(stats) =>
currentDeveloper == null
? ConstrainedBox( ? ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: _DeveloperUnselectedWidget( child: _DeveloperUnselectedWidget(
@@ -229,9 +225,7 @@ class _MainContentSection extends HookConsumerWidget {
if (stats != null) ...[ if (stats != null) ...[
Text( Text(
'Overview', 'Overview',
style: Theme.of( style: Theme.of(context).textTheme.titleLarge?.copyWith(
context,
).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
@@ -245,11 +239,9 @@ class _MainContentSection extends HookConsumerWidget {
children: [ children: [
Text( Text(
'Projects', 'Projects',
style: Theme.of( style: Theme.of(context).textTheme.titleLarge
context, ?.copyWith(
).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface,
color:
Theme.of(context).colorScheme.onSurface,
), ),
), ),
const Spacer(), const Spacer(),
@@ -273,17 +265,13 @@ class _MainContentSection extends HookConsumerWidget {
// Projects List // Projects List
projects.value?.isNotEmpty ?? false projects.value?.isNotEmpty ?? false
? Column( ? Column(
children: children: projects.value!
projects.value!
.map( .map(
(project) => _ProjectListTile( (project) => _ProjectListTile(
project: project, project: project,
publisherName: publisherName:
currentDeveloper! currentDeveloper!.publisher!.name,
.publisher! onProjectSelected: onProjectSelected,
.name,
onProjectSelected:
onProjectSelected,
), ),
) )
.toList(), .toList(),
@@ -294,8 +282,7 @@ class _MainContentSection extends HookConsumerWidget {
child: Text( child: Text(
'No projects available', 'No projects available',
style: TextStyle( style: TextStyle(
color: color: Theme.of(context).colorScheme.onSurface,
Theme.of(context).colorScheme.onSurface,
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -304,8 +291,7 @@ class _MainContentSection extends HookConsumerWidget {
), ),
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (err, stack) => ResponseErrorWidget(
(err, stack) => ResponseErrorWidget(
error: err, error: err,
onRetry: () { onRetry: () {
ref.invalidate( ref.invalidate(
@@ -335,9 +321,7 @@ class DeveloperSelector extends HookConsumerWidget {
final developers = ref.watch(developersProvider); final developers = ref.watch(developersProvider);
final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when( final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
data: data: (data) => data
(data) =>
data
.map( .map(
(item) => DropdownMenuItem<SnDeveloper>( (item) => DropdownMenuItem<SnDeveloper>(
value: item, value: item,
@@ -349,8 +333,7 @@ class DeveloperSelector extends HookConsumerWidget {
), ),
title: Text(item.publisher!.nick), title: Text(item.publisher!.nick),
subtitle: Text('@${item.publisher!.name}'), subtitle: Text('@${item.publisher!.name}'),
trailing: trailing: currentDeveloper?.id == item.id
currentDeveloper?.id == item.id
? const Icon(Icons.check) ? const Icon(Icons.check)
: null, : null,
contentPadding: EdgeInsets.symmetric(horizontal: 8), contentPadding: EdgeInsets.symmetric(horizontal: 8),
@@ -446,8 +429,7 @@ class ProjectSelector extends HookConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final List<DropdownMenuItem<DevProject>> projectsMenu = final List<DropdownMenuItem<DevProject>> projectsMenu = projects.value!
projects.value!
.map( .map(
(item) => DropdownMenuItem<DevProject>( (item) => DropdownMenuItem<DevProject>(
value: item, value: item,
@@ -469,8 +451,7 @@ class ProjectSelector extends HookConsumerWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: trailing: currentProject?.id == item.id
currentProject?.id == item.id
? const Icon(Icons.check) ? const Icon(Icons.check)
: null, : null,
contentPadding: EdgeInsets.symmetric(horizontal: 8), contentPadding: EdgeInsets.symmetric(horizontal: 8),
@@ -496,22 +477,21 @@ class ProjectSelector extends HookConsumerWidget {
final isWider = isWiderScreen(context); final isWider = isWiderScreen(context);
return projectsMenu return projectsMenu
.map( .map(
(e) => (e) => isWider
isWider
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 16, radius: 16,
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
).colorScheme.primary,
child: Text( child: Text(
e.value?.name.isNotEmpty ?? false e.value?.name.isNotEmpty ?? false
? e.value!.name[0].toUpperCase() ? e.value!.name[0].toUpperCase()
: '?', : '?',
style: TextStyle( style: TextStyle(
color: color: Theme.of(context).colorScheme.onPrimary,
Theme.of(context).colorScheme.onPrimary,
), ),
), ),
), ),
@@ -519,8 +499,7 @@ class ProjectSelector extends HookConsumerWidget {
Text( Text(
e.value?.name ?? '?', e.value?.name ?? '?',
style: TextStyle( style: TextStyle(
color: color: Theme.of(
Theme.of(
context, context,
).appBarTheme.foregroundColor, ).appBarTheme.foregroundColor,
), ),
@@ -529,8 +508,7 @@ class ProjectSelector extends HookConsumerWidget {
).padding(right: 8) ).padding(right: 8)
: CircleAvatar( : CircleAvatar(
radius: 16, radius: 16,
backgroundColor: backgroundColor: Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.primary,
child: Text( child: Text(
e.value?.name.isNotEmpty ?? false e.value?.name.isNotEmpty ?? false
? e.value!.name[0].toUpperCase() ? e.value!.name[0].toUpperCase()
@@ -590,8 +568,7 @@ class _ProjectListTile extends HookConsumerWidget {
subtitle: Text(project.description ?? ''), subtitle: Text(project.description ?? ''),
contentPadding: const EdgeInsets.only(left: 16, right: 17), contentPadding: const EdgeInsets.only(left: 16, right: 17),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
value: 'edit', value: 'edit',
child: Row( child: Row(
@@ -608,10 +585,7 @@ class _ProjectListTile extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.delete, color: Colors.red), const Icon(Symbols.delete, color: Colors.red),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text('delete', style: const TextStyle(color: Colors.red)).tr(),
'delete',
style: const TextStyle(color: Colors.red),
).tr(),
], ],
), ),
), ),
@@ -621,8 +595,7 @@ class _ProjectListTile extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'editProject'.tr(), titleText: 'editProject'.tr(),
child: ProjectForm( child: ProjectForm(
publisherName: publisherName, publisherName: publisherName,
@@ -735,6 +708,13 @@ class _DeveloperUnselectedWidget extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (!hasDevelopers) ...[ if (!hasDevelopers) ...[
if (developers.isLoading)
Padding(
padding: const EdgeInsets.all(8),
child: const CircularProgressIndicator(),
)
else
...([
const Icon( const Icon(
Symbols.info, Symbols.info,
fill: 1, fill: 1,
@@ -745,6 +725,7 @@ class _DeveloperUnselectedWidget extends HookConsumerWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
).tr(), ).tr(),
]),
const Gap(24), const Gap(24),
], ],
if (hasDevelopers) if (hasDevelopers)
@@ -818,8 +799,7 @@ class ProjectForm extends HookConsumerWidget {
'description': descriptionController.text, 'description': descriptionController.text,
}; };
final resp = final resp = isEditing
isEditing
? await client.put( ? await client.put(
'/develop/developers/$publisherName/projects/${project!.id}', '/develop/developers/$publisherName/projects/${project!.id}',
data: data, data: data,
@@ -860,8 +840,8 @@ class ProjectForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: slugController, controller: slugController,
@@ -878,8 +858,8 @@ class ProjectForm extends HookConsumerWidget {
} }
return null; return null;
}, },
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: descriptionController, controller: descriptionController,
@@ -892,8 +872,8 @@ class ProjectForm extends HookConsumerWidget {
), ),
minLines: 3, minLines: 3,
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@@ -934,12 +914,9 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: 'enrollDeveloper'.tr(), titleText: 'enrollDeveloper'.tr(),
child: publishers.when( child: publishers.when(
data: data: (items) => items.isEmpty
(items) =>
items.isEmpty
? Center( ? Center(
child: child: Text(
Text(
'noDevelopersToEnroll', 'noDevelopersToEnroll',
textAlign: TextAlign.center, textAlign: TextAlign.center,
).tr(), ).tr(),
@@ -961,8 +938,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (error, _) => ResponseErrorWidget(
(error, _) => ResponseErrorWidget(
error: error, error: error,
onRetry: () => ref.invalidate(publishersManagedProvider), onRetry: () => ref.invalidate(publishersManagedProvider),
), ),

View File

@@ -24,6 +24,16 @@ class ProjectDetailView extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController(initialLength: 2); final tabController = useTabController(initialLength: 2);
final currentDest = useState(0);
useEffect(() {
tabController.addListener(() {
if (tabController.indexIsChanging) {
currentDest.value = tabController.index;
}
});
return null;
});
final isWide = isWideScreen(context); final isWide = isWideScreen(context);
@@ -38,14 +48,13 @@ class ProjectDetailView extends HookConsumerWidget {
child: NavigationRail( child: NavigationRail(
extended: isWiderScreen(context), extended: isWiderScreen(context),
scrollable: true, scrollable: true,
labelType: labelType: isWiderScreen(context)
isWiderScreen(context)
? null ? null
: NavigationRailLabelType.selected, : NavigationRailLabelType.selected,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
selectedIndex: tabController.index, selectedIndex: currentDest.value,
onDestinationSelected: onDestinationSelected: (index) =>
(index) => tabController.animateTo(index), tabController.animateTo(index),
destinations: [ destinations: [
NavigationRailDestination( NavigationRailDestination(
icon: Icon(Icons.apps), icon: Icon(Icons.apps),

View File

@@ -33,7 +33,12 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
Future<List<SnWebArticle>> fetch() async { Future<List<SnWebArticle>> fetch() async {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final queryParams = {'limit': pageSize, 'offset': fetchedCount.toString()}; final queryParams = {
'limit': pageSize,
'offset': fetchedCount.toString(),
'feedId': arg.feedId,
'publisherId': arg.publisherId,
}..removeWhere((key, value) => value == null);
try { try {
final response = await client.get( final response = await client.get(
@@ -41,11 +46,8 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
queryParameters: queryParams, queryParameters: queryParams,
); );
final articles = final articles = response.data
response.data .map((json) => SnWebArticle.fromJson(json as Map<String, dynamic>))
.map(
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
)
.cast<SnWebArticle>() .cast<SnWebArticle>()
.toList(); .toList();
@@ -81,6 +83,7 @@ class SliverArticlesList extends ConsumerWidget {
ArticleListQuery(feedId: feedId, publisherId: publisherId), ArticleListQuery(feedId: feedId, publisherId: publisherId),
); );
return PaginationList( return PaginationList(
spacing: 12,
provider: provider, provider: provider,
notifier: provider.notifier, notifier: provider.notifier,
isRefreshable: false, isRefreshable: false,
@@ -184,14 +187,12 @@ class ArticlesScreen extends ConsumerWidget {
), ),
); );
}, },
loading: loading: () => AppScaffold(
() => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(title: const Text('Articles')), appBar: AppBar(title: const Text('Articles')),
body: const Center(child: CircularProgressIndicator()), body: const Center(child: CircularProgressIndicator()),
), ),
error: error: (err, stack) => AppScaffold(
(err, stack) => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(title: const Text('Articles')), appBar: AppBar(title: const Text('Articles')),
body: Center(child: Text('Error: $err')), body: Center(child: Text('Error: $err')),

View File

@@ -44,8 +44,7 @@ class MarketplaceWebFeedContentNotifier
queryParameters: queryParams, queryParameters: queryParams,
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final articles = final articles = response.data
response.data
.map((json) => SnWebArticle.fromJson(json)) .map((json) => SnWebArticle.fromJson(json))
.cast<SnWebArticle>() .cast<SnWebArticle>()
.toList(); .toList();
@@ -116,8 +115,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
// Feed meta // Feed meta
feed feed
.when( .when(
data: data: (data) => Column(
(data) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text(data.description ?? 'descriptionNone'.tr()), Text(data.description ?? 'descriptionNone'.tr()),
@@ -149,10 +147,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
// Articles list // Articles list
Expanded( Expanded(
child: PaginationList( child: PaginationList(
spacing: 8,
padding: EdgeInsets.symmetric(vertical: 8),
provider: marketplaceWebFeedContentNotifierProvider(id), provider: marketplaceWebFeedContentNotifierProvider(id),
notifier: marketplaceWebFeedContentNotifierProvider(id).notifier, notifier: marketplaceWebFeedContentNotifierProvider(id).notifier,
itemBuilder: (context, index, article) { itemBuilder: (context, index, article) {
return WebArticleCard(article: article); return WebArticleCard(article: article).padding(horizontal: 12);
}, },
), ),
), ),
@@ -165,10 +165,8 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
), ),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: subscribed.when( child: subscribed.when(
data: data: (isSubscribed) => FilledButton.icon(
(isSubscribed) => FilledButton.icon( onPressed: isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
onPressed:
isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
icon: Icon( icon: Icon(
isSubscribed ? Symbols.remove_circle : Symbols.add_circle, isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
), ),
@@ -176,14 +174,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(), isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
), ),
), ),
loading: loading: () => const SizedBox(
() => const SizedBox(
height: 32, height: 32,
width: 32, width: 32,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
), ).center(),
error: error: (_, _) => OutlinedButton.icon(
(_, _) => OutlinedButton.icon(
onPressed: subscribeToFeed, onPressed: subscribeToFeed,
icon: const Icon(Symbols.add_circle), icon: const Icon(Symbols.add_circle),
label: Text('subscribe').tr(), label: Text('subscribe').tr(),

View File

@@ -12,7 +12,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider( final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider.autoDispose(
MarketplaceWebFeedsNotifier.new, MarketplaceWebFeedsNotifier.new,
); );
@@ -38,8 +38,7 @@ class MarketplaceWebFeedsNotifier extends AsyncNotifier<List<SnWebFeed>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final feeds = final feeds = response.data
response.data
.map((e) => SnWebFeed.fromJson(e)) .map((e) => SnWebFeed.fromJson(e))
.cast<SnWebFeed>() .cast<SnWebFeed>()
.toList(); .toList();
@@ -92,8 +91,8 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24), const EdgeInsets.symmetric(horizontal: 24),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
trailing: [ trailing: [
if (query.value != null && query.value!.isNotEmpty) if (query.value != null && query.value!.isNotEmpty)
IconButton( IconButton(
@@ -128,6 +127,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemBuilder: (context, index, feed) { itemBuilder: (context, index, feed) {
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(feed.title), title: Text(feed.title),
subtitle: Text(feed.description ?? ''), subtitle: Text(feed.description ?? ''),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@@ -23,7 +23,7 @@ class DiscoveryRealmsScreen extends HookConsumerWidget {
children: [ children: [
CustomScrollView( CustomScrollView(
slivers: [ slivers: [
SliverGap(80), SliverGap(88),
SliverRealmList( SliverRealmList(
query: currentQuery.value, query: currentQuery.value,
key: ValueKey(currentQuery.value), key: ValueKey(currentQuery.value),

View File

@@ -102,7 +102,7 @@ class ExploreScreen extends HookConsumerWidget {
// Listen for post creation events to refresh activities // Listen for post creation events to refresh activities
useEffect(() { useEffect(() {
final subscription = eventBus.on<PostCreatedEvent>().listen((event) { final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
ref.invalidate(activityListProvider); ref.read(activityListProvider.notifier).refresh();
}); });
return subscription.cancel; return subscription.cancel;
}, []); }, []);
@@ -183,25 +183,13 @@ class ExploreScreen extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.category), const Icon(Symbols.category),
const Gap(12), const Gap(12),
Text('categories').tr(), Text('categoriesAndTags').tr(),
], ],
), ),
onTap: () { onTap: () {
context.pushNamed('postCategories'); context.pushNamed('postCategories');
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -490,25 +478,13 @@ class ExploreScreen extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.category), const Icon(Symbols.category),
const Gap(12), const Gap(12),
Text('categories').tr(), Text('categoriesAndTags').tr(),
], ],
), ),
onTap: () { onTap: () {
context.pushNamed('postCategories'); context.pushNamed('postCategories');
}, },
), ),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.label),
const Gap(12),
Text('tags').tr(),
],
),
onTap: () {
context.pushNamed('postTags');
},
),
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [

View File

@@ -10,9 +10,9 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/file_references.dart'; import 'package:island/pods/drive/file_references.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/upload_tasks.dart'; import 'package:island/pods/drive/upload_tasks.dart';
import 'package:island/models/drive_task.dart'; import 'package:island/models/drive_task.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
@@ -120,8 +120,9 @@ class FileDetailScreen extends HookConsumerWidget {
child: SizedBox( child: SizedBox(
width: 400, width: 400,
child: Material( child: Material(
color: color: Theme.of(
Theme.of(context).colorScheme.surfaceContainer, context,
).colorScheme.surfaceContainer,
elevation: 8, elevation: 8,
child: FileInfoSheet( child: FileInfoSheet(
item: item, item: item,
@@ -176,13 +177,11 @@ class FileDetailScreen extends HookConsumerWidget {
actions.add( actions.add(
IconButton( IconButton(
icon: Icon(Icons.link), icon: Icon(Icons.link),
onPressed: onPressed: () => showModalBottomSheet(
() => showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'File References', titleText: 'File References',
child: ReferencesList(fileId: item.id), child: ReferencesList(fileId: item.id),
), ),
@@ -300,8 +299,7 @@ class ReferencesList extends ConsumerWidget {
final asyncReferences = ref.watch(fileReferencesProvider(fileId)); final asyncReferences = ref.watch(fileReferencesProvider(fileId));
return asyncReferences.when( return asyncReferences.when(
data: data: (references) => ListView.builder(
(references) => ListView.builder(
itemCount: references.length, itemCount: references.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final reference = references[index]; final reference = references[index];
@@ -317,10 +315,7 @@ class ReferencesList extends ConsumerWidget {
fontSize: 13, fontSize: 13,
), ),
), ),
Text( Text(reference.id, style: GoogleFonts.robotoMono(fontSize: 13)),
reference.id,
style: GoogleFonts.robotoMono(fontSize: 13),
),
], ],
), ),
subtitle: Row( subtitle: Row(
@@ -335,8 +330,8 @@ class ReferencesList extends ConsumerWidget {
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: error: (error, _) =>
(error, _) => Center(child: Text('Error loading references: $error')), Center(child: Text('Error loading references: $error')),
); );
} }
} }

View File

@@ -7,7 +7,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/file_pool.dart'; import 'package:island/models/file_pool.dart';
import 'package:island/pods/file_list.dart'; import 'package:island/pods/drive/file_list.dart';
import 'package:island/services/file_uploader.dart'; import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@@ -40,27 +40,20 @@ class FileListScreen extends HookConsumerWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.bar_chart), icon: const Icon(Symbols.bar_chart),
onPressed: onPressed: () =>
() => _showUsageSheet( _showUsageSheet(context, usageAsync.value, quotaAsync.value),
context,
usageAsync.value,
quotaAsync.value,
),
), ),
const Gap(8), const Gap(8),
], ],
), ),
body: usageAsync.when( body: usageAsync.when(
data: data: (usage) => quotaAsync.when(
(usage) => quotaAsync.when( data: (quota) => FileListView(
data:
(quota) => FileListView(
usage: usage, usage: usage,
quota: quota, quota: quota,
currentPath: currentPath, currentPath: currentPath,
selectedPool: selectedPool, selectedPool: selectedPool,
onPickAndUpload: onPickAndUpload: () => _pickAndUploadFile(
() => _pickAndUploadFile(
ref, ref,
currentPath.value, currentPath.value,
selectedPool.value?.id, selectedPool.value?.id,
@@ -158,8 +151,7 @@ class FileListScreen extends HookConsumerWidget {
await showDialog( await showDialog(
context: context, context: context,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: const Text('Navigate to Directory'), title: const Text('Navigate to Directory'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -207,8 +199,7 @@ class FileListScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'Usage Overview', titleText: 'Usage Overview',
child: UsageOverviewWidget( child: UsageOverviewWidget(
usage: usage, usage: usage,

View File

@@ -18,11 +18,92 @@ import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart'; part 'notification.g.dart';
class SkeletonNotificationTile extends StatelessWidget {
const SkeletonNotificationTile({super.key});
@override
Widget build(BuildContext context) {
const fakeTitle = 'New notification';
const fakeSubtitle = 'You have a new message from someone';
const fakeContent =
'This is a preview of the notification content. It may contain formatted text.';
const List<String> fakeImageIds = []; // Empty list for no images
const String? fakePfp = null; // No profile picture
return ListTile(
isThreeLine: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: fakePfp != null
? ProfilePictureWidget(fileId: fakePfp, radius: 20)
: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(
Symbols.notifications,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
title: const Text(fakeTitle),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(fakeSubtitle).bold(),
Row(
spacing: 6,
children: [
Text('Loading...').fontSize(11),
Skeleton.ignore(child: Text('·').fontSize(11).bold()),
Text('Now').fontSize(11),
],
).opacity(0.75).padding(bottom: 4),
MarkdownTextContent(
content: fakeContent,
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
),
if (fakeImageIds.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: fakeImageIds.map((imageId) {
return SizedBox(
width: 80,
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CloudImageWidget(
fileId: imageId,
aspectRatio: 1,
fit: BoxFit.cover,
),
),
);
}).toList(),
),
),
],
),
trailing: Container(
width: 12,
height: 12,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
onTap: () {},
);
}
}
@riverpod @riverpod
class NotificationUnreadCountNotifier class NotificationUnreadCountNotifier
extends _$NotificationUnreadCountNotifier { extends _$NotificationUnreadCountNotifier {
@@ -82,7 +163,7 @@ class NotificationUnreadCountNotifier
} }
} }
final notificationListProvider = AsyncNotifierProvider( final notificationListProvider = AsyncNotifierProvider.autoDispose(
NotificationListNotifier.new, NotificationListNotifier.new,
); );
@@ -182,6 +263,7 @@ class NotificationSheet extends HookConsumerWidget {
child: PaginationList( child: PaginationList(
provider: notificationListProvider, provider: notificationListProvider,
notifier: notificationListProvider.notifier, notifier: notificationListProvider.notifier,
footerSkeletonChild: const SkeletonNotificationTile(),
itemBuilder: (context, index, notification) { itemBuilder: (context, index, notification) {
final pfp = notification.meta['pfp'] as String?; final pfp = notification.meta['pfp'] as String?;
final images = notification.meta['images'] as List?; final images = notification.meta['images'] as List?;

View File

@@ -18,12 +18,10 @@ _PostComposeInitialState _$PostComposeInitialStateFromJson(
.toList() ?? .toList() ??
const [], const [],
visibility: (json['visibility'] as num?)?.toInt(), visibility: (json['visibility'] as num?)?.toInt(),
replyingTo: replyingTo: json['replying_to'] == null
json['replying_to'] == null
? null ? null
: SnPost.fromJson(json['replying_to'] as Map<String, dynamic>), : SnPost.fromJson(json['replying_to'] as Map<String, dynamic>),
forwardingTo: forwardingTo: json['forwarding_to'] == null
json['forwarding_to'] == null
? null ? null
: SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>), : SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>),
); );

View File

@@ -2,73 +2,64 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart'; import 'package:island/pods/post/post_categories.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
// Post Categories Notifier class PostCategoriesListScreen extends HookConsumerWidget {
final postCategoriesProvider = AsyncNotifierProvider.autoDispose<
PostCategoriesNotifier,
List<SnPostCategory>
>(PostCategoriesNotifier.new);
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
with AsyncPaginationController<SnPostCategory> {
@override
Future<List<SnPostCategory>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/categories',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostCategory.fromJson(json)).toList();
}
}
// Post Tags Notifier
final postTagsProvider =
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
PostTagsNotifier.new,
);
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
with AsyncPaginationController<SnPostTag> {
@override
Future<List<SnPostTag>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts/tags',
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPostTag.fromJson(json)).toList();
}
}
class PostCategoriesListScreen extends ConsumerWidget {
const PostCategoriesListScreen({super.key}); const PostCategoriesListScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold( return DefaultTabController(
appBar: AppBar(title: const Text('categories').tr()), length: 2,
body: PaginationList( child: AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: const Text('categoriesAndTags').tr(),
bottom: TabBar(
tabs: [
Tab(
child: Text(
'categories'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
),
Tab(
child: Text(
'tags'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
),
],
),
),
body: const TabBarView(children: [_CategoriesTab(), _TagsTab()]),
),
);
}
}
class _CategoriesTab extends ConsumerWidget {
const _CategoriesTab();
@override
Widget build(BuildContext context, WidgetRef ref) {
return PaginationList(
provider: postCategoriesProvider, provider: postCategoriesProvider,
notifier: postCategoriesProvider.notifier, notifier: postCategoriesProvider.notifier,
footerSkeletonMaxWidth: 640,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemBuilder: (context, index, category) { itemBuilder: (context, index, category) {
return ListTile( return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: ListTile(
leading: const Icon(Symbols.category), leading: const Icon(Symbols.category),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
@@ -80,26 +71,29 @@ class PostCategoriesListScreen extends ConsumerWidget {
pathParameters: {'slug': category.slug}, pathParameters: {'slug': category.slug},
); );
}, },
),
),
); );
}, },
),
); );
} }
} }
class PostTagsListScreen extends ConsumerWidget { class _TagsTab extends ConsumerWidget {
const PostTagsListScreen({super.key}); const _TagsTab();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return AppScaffold( return PaginationList(
appBar: AppBar(title: const Text('tags').tr()),
body: PaginationList(
provider: postTagsProvider, provider: postTagsProvider,
notifier: postTagsProvider.notifier, notifier: postTagsProvider.notifier,
footerSkeletonMaxWidth: 640,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemBuilder: (context, index, tag) { itemBuilder: (context, index, tag) {
return ListTile( return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: ListTile(
title: Text(tag.name ?? '#${tag.slug}'), title: Text(tag.name ?? '#${tag.slug}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.label), leading: const Icon(Symbols.label),
@@ -111,9 +105,10 @@ class PostTagsListScreen extends ConsumerWidget {
pathParameters: {'slug': tag.slug}, pathParameters: {'slug': tag.slug},
); );
}, },
),
),
); );
}, },
),
); );
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post_category.dart'; import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart'; import 'package:island/models/post_tag.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_list.dart'; import 'package:island/widgets/post/post_list.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
@@ -82,15 +83,15 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final postCategory = final postCategory = isCategory
isCategory ? ref.watch(postCategoryProvider(slug)) : null; ? ref.watch(postCategoryProvider(slug))
: null;
final postTag = isCategory ? null : ref.watch(postTagProvider(slug)); final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
final subscriptionStatus = ref.watch( final subscriptionStatus = ref.watch(
postCategorySubscriptionStatusProvider(slug, isCategory), postCategorySubscriptionStatusProvider(slug, isCategory),
); );
final postFilterTitle = final postFilterTitle = isCategory
isCategory
? postCategory?.value?.categoryDisplayTitle ?? 'loading' ? postCategory?.value?.categoryDisplayTitle ?? 'loading'
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading'; : postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
@@ -108,8 +109,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
child: Card( child: Card(
margin: EdgeInsets.only(top: 8), margin: EdgeInsets.only(top: 8),
child: postCategory!.when( child: postCategory!.when(
data: data: (category) => Column(
(category) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
@@ -118,9 +118,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
Text('A category'), Text('A category'),
const Gap(8), const Gap(8),
subscriptionStatus.when( subscriptionStatus.when(
data: data: (isSubscribed) => isSubscribed
(isSubscribed) =>
isSubscribed
? FilledButton.icon( ? FilledButton.icon(
onPressed: () async { onPressed: () async {
await _unsubscribeFromCategoryOrTag( await _unsubscribeFromCategoryOrTag(
@@ -129,9 +127,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
isCategory: isCategory, isCategory: isCategory,
); );
}, },
icon: const Icon( icon: const Icon(Symbols.remove_circle),
Symbols.remove_circle,
),
label: Text('unsubscribe'.tr()), label: Text('unsubscribe'.tr()),
) )
: FilledButton.icon( : FilledButton.icon(
@@ -142,28 +138,20 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
isCategory: isCategory, isCategory: isCategory,
); );
}, },
icon: const Icon( icon: const Icon(Symbols.add_circle),
Symbols.add_circle,
),
label: Text('subscribe'.tr()), label: Text('subscribe'.tr()),
), ),
error: error: (error, _) =>
(error, _) => Text( Text('Error loading subscription status'),
'Error loading subscription status', loading: () =>
),
loading:
() =>
CircularProgressIndicator().center(), CircularProgressIndicator().center(),
), ),
], ],
).padding(horizontal: 24, vertical: 16), ).padding(horizontal: 24, vertical: 16),
error: error: (error, _) => ResponseErrorWidget(
(error, _) => ResponseErrorWidget(
error: error, error: error,
onRetry: onRetry: () =>
() => ref.invalidate( ref.invalidate(postCategoryProvider(slug)),
postCategoryProvider(slug),
),
), ),
loading: () => ResponseLoadingWidget(), loading: () => ResponseLoadingWidget(),
), ),
@@ -179,8 +167,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
child: Card( child: Card(
margin: EdgeInsets.only(top: 8), margin: EdgeInsets.only(top: 8),
child: postTag!.when( child: postTag!.when(
data: data: (tag) => Column(
(tag) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
@@ -189,9 +176,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
Text('A tag'), Text('A tag'),
const Gap(8), const Gap(8),
subscriptionStatus.when( subscriptionStatus.when(
data: data: (isSubscribed) => isSubscribed
(isSubscribed) =>
isSubscribed
? FilledButton.icon( ? FilledButton.icon(
onPressed: () async { onPressed: () async {
await _unsubscribeFromCategoryOrTag( await _unsubscribeFromCategoryOrTag(
@@ -200,9 +185,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
isCategory: isCategory, isCategory: isCategory,
); );
}, },
icon: const Icon( icon: const Icon(Symbols.remove_circle),
Symbols.remove_circle,
),
label: Text('unsubscribe'.tr()), label: Text('unsubscribe'.tr()),
) )
: FilledButton.icon( : FilledButton.icon(
@@ -213,26 +196,19 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
isCategory: isCategory, isCategory: isCategory,
); );
}, },
icon: const Icon( icon: const Icon(Symbols.add_circle),
Symbols.add_circle,
),
label: Text('subscribe'.tr()), label: Text('subscribe'.tr()),
), ),
error: error: (error, _) =>
(error, _) => Text( Text('Error loading subscription status'),
'Error loading subscription status', loading: () =>
),
loading:
() =>
CircularProgressIndicator().center(), CircularProgressIndicator().center(),
), ),
], ],
).padding(horizontal: 24, vertical: 16), ).padding(horizontal: 24, vertical: 16),
error: error: (error, _) => ResponseErrorWidget(
(error, _) => ResponseErrorWidget(
error: error, error: error,
onRetry: onRetry: () => ref.invalidate(postTagProvider(slug)),
() => ref.invalidate(postTagProvider(slug)),
), ),
loading: () => ResponseLoadingWidget(), loading: () => ResponseLoadingWidget(),
), ),
@@ -242,8 +218,11 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
), ),
const SliverGap(4), const SliverGap(4),
SliverPostList( SliverPostList(
query: PostListQuery(
categories: isCategory ? [slug] : null, categories: isCategory ? [slug] : null,
tags: isCategory ? null : [slug], tags: isCategory ? null : [slug],
),
maxWidth: 540 + 16, maxWidth: 540 + 16,
), ),
SliverGap(MediaQuery.of(context).padding.bottom + 8), SliverGap(MediaQuery.of(context).padding.bottom + 8),

View File

@@ -3,102 +3,19 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/extended_refresh_indicator.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_item_skeleton.dart';
import 'package:island/pods/paging.dart'; import 'package:island/widgets/posts/post_filter.dart';
import 'package:gap/gap.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
final postSearchProvider = AsyncNotifierProvider.autoDispose( const kSearchPostListId = 'search';
PostSearchNotifier.new,
);
class PostSearchNotifier extends AsyncNotifier<List<SnPost>>
with AsyncPaginationController<SnPost> {
static const int _pageSize = 20;
String _currentQuery = '';
String? _pubName;
String? _realm;
int? _type;
List<String>? _categories;
List<String>? _tags;
bool _shuffle = false;
bool? _pinned;
@override
FutureOr<List<SnPost>> build() async {
// Initial state is empty if no query/filters, or fetch if needed
// But original logic allowed initial empty state.
// Let's replicate original logic: return empty list initially if no query.
return [];
}
Future<void> search(
String query, {
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
bool shuffle = false,
bool? pinned,
}) async {
_currentQuery = query.trim();
_pubName = pubName;
_realm = realm;
_type = type;
_categories = categories;
_tags = tags;
_shuffle = shuffle;
_pinned = pinned;
final hasFilters =
pubName != null ||
realm != null ||
type != null ||
categories != null ||
tags != null ||
shuffle ||
pinned != null;
if (_currentQuery.isEmpty && !hasFilters) {
state = const AsyncData([]);
totalCount = null;
return;
}
await refresh();
}
@override
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/sphere/posts',
queryParameters: {
'query': _currentQuery,
'offset': fetchedCount,
'take': _pageSize,
'vector': false,
if (_pubName != null) 'pub': _pubName,
if (_realm != null) 'realm': _realm,
if (_type != null) 'type': _type,
if (_tags != null) 'tags': _tags,
if (_categories != null) 'categories': _categories,
if (_shuffle) 'shuffle': true,
if (_pinned != null) 'pinned': _pinned,
},
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final data = response.data as List;
return data.map((json) => SnPost.fromJson(json)).toList();
}
}
class PostSearchScreen extends HookConsumerWidget { class PostSearchScreen extends HookConsumerWidget {
const PostSearchScreen({super.key}); const PostSearchScreen({super.key});
@@ -111,11 +28,16 @@ class PostSearchScreen extends HookConsumerWidget {
final showFilters = useState(false); final showFilters = useState(false);
final pubNameController = useTextEditingController(); final pubNameController = useTextEditingController();
final realmController = useTextEditingController(); final realmController = useTextEditingController();
final typeValue = useState<int?>(null);
final selectedCategories = useState<List<String>>([]); // State variables for PostFilterWidget
final selectedTags = useState<List<String>>([]); final categoryTabController = useTabController(initialLength: 3);
final shuffleValue = useState(false);
final pinnedValue = useState<bool?>(null); // Single query state
final queryState = useState(const PostListQuery());
final noti = ref.read(
postListProvider(PostListQueryConfig(id: kSearchPostListId)).notifier,
);
useEffect(() { useEffect(() {
return () { return () {
@@ -126,186 +48,208 @@ class PostSearchScreen extends HookConsumerWidget {
}; };
}, []); }, []);
void onSearchChanged(String query) { void onSearchChanged(String query, {bool skipDebounce = false}) {
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel(); queryState.value = queryState.value.copyWith(queryTerm: query);
if (skipDebounce) {
noti.applyFilter(queryState.value);
return;
}
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
debounceTimer.value = Timer(debounce, () { debounceTimer.value = Timer(debounce, () {
ref.read(postSearchProvider.notifier).search(query); noti.applyFilter(queryState.value);
}); });
} }
void onSearchWithFilters(String query) { void toggleFilterDisplay() {
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
debounceTimer.value = Timer(debounce, () {
ref
.read(postSearchProvider.notifier)
.search(
query,
pubName:
pubNameController.text.isNotEmpty
? pubNameController.text
: null,
realm:
realmController.text.isNotEmpty ? realmController.text : null,
type: typeValue.value,
categories:
selectedCategories.value.isNotEmpty
? selectedCategories.value
: null,
tags: selectedTags.value.isNotEmpty ? selectedTags.value : null,
shuffle: shuffleValue.value,
pinned: pinnedValue.value,
);
});
}
void toggleFilters() {
showFilters.value = !showFilters.value; showFilters.value = !showFilters.value;
} }
void applyFilters() {
onSearchWithFilters(searchController.text);
}
void clearFilters() {
pubNameController.clear();
realmController.clear();
typeValue.value = null;
selectedCategories.value = [];
selectedTags.value = [];
shuffleValue.value = false;
pinnedValue.value = null;
onSearchChanged(searchController.text);
}
Widget buildFilterPanel() { Widget buildFilterPanel() {
return Card( return PostFilterWidget(
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 8), categoryTabController: categoryTabController,
child: Padding( initialQuery: queryState.value,
padding: EdgeInsets.all(16), onQueryChanged: (newQuery) {
child: Column( queryState.value = newQuery;
crossAxisAlignment: CrossAxisAlignment.start, noti.applyFilter(newQuery);
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'filters'.tr(),
style: Theme.of(context).textTheme.titleMedium,
).padding(left: 4),
Row(
children: [
TextButton(
onPressed: applyFilters,
child: Text('apply'.tr()),
),
TextButton(
onPressed: clearFilters,
child: Text('clear'.tr()),
),
],
),
],
),
SizedBox(height: 16),
TextField(
controller: pubNameController,
decoration: InputDecoration(
labelText: 'pubName'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
),
onChanged:
(value) => onSearchWithFilters(searchController.text),
),
SizedBox(height: 8),
TextField(
controller: realmController,
decoration: InputDecoration(
labelText: 'realm'.tr(),
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
),
onChanged:
(value) => onSearchWithFilters(searchController.text),
),
SizedBox(height: 8),
Row(
children: [
Checkbox(
value: shuffleValue.value,
onChanged: (value) {
shuffleValue.value = value ?? false;
onSearchWithFilters(searchController.text);
}, },
), hideSearch: true,
Text('shuffle'.tr()),
],
),
Row(
children: [
Checkbox(
value: pinnedValue.value ?? false,
onChanged: (value) {
pinnedValue.value = value;
onSearchWithFilters(searchController.text);
},
),
Text('pinned'.tr()),
],
),
],
),
),
); );
} }
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar( appBar: AppBar(
title: Row( title: Text('searchPosts'.tr()),
children: [ actions: [
Expanded( if (!isWideScreen(context))
child: TextField(
controller: searchController,
decoration: InputDecoration(
hintText: 'search'.tr(),
border: InputBorder.none,
hintStyle: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
onChanged: onSearchChanged,
onSubmitted: (value) {
onSearchWithFilters(value);
},
autofocus: true,
),
),
IconButton( IconButton(
icon: Icon( icon: Icon(
showFilters.value showFilters.value
? Icons.filter_alt ? Icons.filter_alt
: Icons.filter_alt_outlined, : Icons.filter_alt_outlined,
), ),
onPressed: toggleFilters, onPressed: toggleFilterDisplay,
tooltip: 'toggleFilters'.tr(), tooltip: 'toggleFilters'.tr(),
), ),
], ],
), ),
),
body: Consumer( body: Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final searchState = ref.watch(postSearchProvider); final searchState = ref.watch(
postListProvider(PostListQueryConfig(id: kSearchPostListId)),
);
return CustomScrollView( return isWideScreen(context)
? Row(
children: [
Flexible(
flex: 4,
child: ExtendedRefreshIndicator(
onRefresh: noti.refresh,
child: CustomScrollView(
slivers: [ slivers: [
SliverGap(16),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: SearchBar(
elevation: WidgetStateProperty.all(4),
controller: searchController,
hintText: 'search'.tr(),
leading: const Icon(Icons.search),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24),
),
onChanged: onSearchChanged,
onSubmitted: (value) {
onSearchChanged(value, skipDebounce: true);
},
),
),
),
const SliverGap(12),
PaginationList(
provider: postListProvider(
PostListQueryConfig(id: kSearchPostListId),
),
notifier: postListProvider(
PostListQueryConfig(id: kSearchPostListId),
).notifier,
isSliver: true,
isRefreshable: false,
footerSkeletonChild: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
child: const PostItemSkeleton(),
),
itemBuilder: (context, index, post) {
return Card(
margin: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: PostActionableItem(
item: post,
borderRadius: 8,
),
);
},
),
if (searchState.value?.isEmpty == true &&
searchController.text.isNotEmpty &&
!searchState.isLoading)
SliverFillRemaining(
child: Center(
child: Text('noResultsFound'.tr()),
),
),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
),
],
).padding(left: 8),
),
),
Flexible(
flex: 3,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Gap(16),
Card(
margin: EdgeInsets.symmetric(horizontal: 8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
const Icon(
Symbols.tune,
).padding(horizontal: 8),
Expanded(
child: Text(
'filters'.tr(),
style: Theme.of(
context,
).textTheme.bodyLarge,
),
),
IconButton(
icon: Icon(
Symbols.filter_alt,
fill: showFilters.value ? 1 : null,
),
onPressed: toggleFilterDisplay,
tooltip: 'toggleFilters'.tr(),
),
const Gap(4),
],
),
),
),
const Gap(8),
if (showFilters.value) buildFilterPanel(),
],
),
),
),
),
],
)
: CustomScrollView(
slivers: [
const SliverGap(4),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: SearchBar(
elevation: WidgetStateProperty.all(4),
controller: searchController,
hintText: 'search'.tr(),
leading: const Icon(Icons.search),
padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24),
),
onChanged: onSearchChanged,
onSubmitted: (value) {
onSearchChanged(value, skipDebounce: true);
},
),
),
),
if (showFilters.value) if (showFilters.value)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Center( child: Center(
@@ -315,13 +259,19 @@ class PostSearchScreen extends HookConsumerWidget {
), ),
), ),
), ),
// Use PaginationList with isSliver=true
PaginationList( PaginationList(
provider: postSearchProvider, provider: postListProvider(
notifier: postSearchProvider.notifier, PostListQueryConfig(id: kSearchPostListId),
),
notifier: postListProvider(
PostListQueryConfig(id: kSearchPostListId),
).notifier,
isSliver: true, isSliver: true,
isRefreshable: isRefreshable: false,
false, // CustomScrollView handles refreshing usually, but here we don't have PullToRefresh footerSkeletonChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: const PostItemSkeleton(),
),
itemBuilder: (context, index, post) { itemBuilder: (context, index, post) {
return Center( return Center(
child: ConstrainedBox( child: ConstrainedBox(
@@ -331,7 +281,10 @@ class PostSearchScreen extends HookConsumerWidget {
horizontal: 8, horizontal: 8,
vertical: 4, vertical: 4,
), ),
child: PostActionableItem(item: post, borderRadius: 8), child: PostActionableItem(
item: post,
borderRadius: 8,
),
), ),
), ),
); );

View File

@@ -11,6 +11,7 @@ import 'package:island/models/account.dart';
import 'package:island/models/heatmap.dart'; import 'package:island/models/heatmap.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/services/color.dart'; import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
@@ -22,6 +23,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/content/markdown.dart';
import 'package:island/widgets/post/post_list.dart'; import 'package:island/widgets/post/post_list.dart';
import 'package:island/widgets/activity_heatmap.dart'; import 'package:island/widgets/activity_heatmap.dart';
import 'package:island/widgets/posts/post_filter.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:island/services/color_extraction.dart'; import 'package:island/services/color_extraction.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -82,8 +84,9 @@ class _PublisherBasisWidget extends StatelessWidget {
size: 12, size: 12,
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).colorScheme.onPrimary,
), ),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
).colorScheme.primary,
offset: Offset(0, 48), offset: Offset(0, 48),
child: ProfilePictureWidget( child: ProfilePictureWidget(
file: data.picture, file: data.picture,
@@ -121,8 +124,9 @@ class _PublisherBasisWidget extends StatelessWidget {
size: 16, size: 16,
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).colorScheme.onPrimary,
), ),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.primary, context,
).colorScheme.primary,
offset: Offset(0, 48), offset: Offset(0, 48),
child: ProfilePictureWidget( child: ProfilePictureWidget(
file: data.picture, file: data.picture,
@@ -201,10 +205,8 @@ class _PublisherBasisWidget extends StatelessWidget {
), ),
subStatus subStatus
.when( .when(
data: data: (status) => FilledButton.icon(
(status) => FilledButton.icon( onPressed: subscribing.value
onPressed:
subscribing.value
? null ? null
: (status.isSubscribed : (status.isSubscribed
? unsubscribe ? unsubscribe
@@ -214,8 +216,7 @@ class _PublisherBasisWidget extends StatelessWidget {
? Symbols.remove_circle ? Symbols.remove_circle
: Symbols.add_circle, : Symbols.add_circle,
), ),
label: label: Text(
Text(
status.isSubscribed status.isSubscribed
? 'unsubscribe' ? 'unsubscribe'
: 'subscribe', : 'subscribe',
@@ -227,8 +228,7 @@ class _PublisherBasisWidget extends StatelessWidget {
), ),
), ),
error: (_, _) => const SizedBox(), error: (_, _) => const SizedBox(),
loading: loading: () => const SizedBox(
() => const SizedBox(
height: 36, height: 36,
child: Center( child: Center(
child: SizedBox( child: SizedBox(
@@ -333,9 +333,7 @@ class _PublisherHeatmapWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return heatmap.when( return heatmap.when(
data: data: (data) => data != null
(data) =>
data != null
? ActivityHeatmapWidget( ? ActivityHeatmapWidget(
heatmap: data, heatmap: data,
forceDense: forceDense, forceDense: forceDense,
@@ -347,271 +345,6 @@ class _PublisherHeatmapWidget extends StatelessWidget {
} }
} }
class _PublisherCategoryTabWidget extends StatelessWidget {
final TabController categoryTabController;
final ValueNotifier<bool?> includeReplies;
final ValueNotifier<bool> mediaOnly;
final ValueNotifier<String?> queryTerm;
final ValueNotifier<String?> order;
final ValueNotifier<bool> orderDesc;
final ValueNotifier<int?> periodStart;
final ValueNotifier<int?> periodEnd;
final ValueNotifier<bool> showAdvancedFilters;
const _PublisherCategoryTabWidget({
required this.categoryTabController,
required this.includeReplies,
required this.mediaOnly,
required this.queryTerm,
required this.order,
required this.orderDesc,
required this.periodStart,
required this.periodEnd,
required this.showAdvancedFilters,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
children: [
TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
const Divider(height: 1),
Column(
children: [
Row(
children: [
Expanded(
child: CheckboxListTile(
title: Text('reply'.tr()),
value: includeReplies.value,
tristate: true,
onChanged: (value) {
// Cycle through: null -> false -> true -> null
if (includeReplies.value == null) {
includeReplies.value = false;
} else if (includeReplies.value == false) {
includeReplies.value = true;
} else {
includeReplies.value = null;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.reply),
),
),
Expanded(
child: CheckboxListTile(
title: Text('attachments'.tr()),
value: mediaOnly.value,
onChanged: (value) {
if (value != null) {
mediaOnly.value = value;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.attachment),
),
),
],
),
CheckboxListTile(
title: Text('descendingOrder'.tr()),
value: orderDesc.value,
onChanged: (value) {
if (value != null) {
orderDesc.value = value;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.sort),
),
],
),
const Divider(height: 1),
ListTile(
title: Text('advancedFilters'.tr()),
leading: const Icon(Symbols.filter_list),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
trailing: Icon(
showAdvancedFilters.value
? Symbols.expand_less
: Symbols.expand_more,
),
onTap: () {
showAdvancedFilters.value = !showAdvancedFilters.value;
},
),
if (showAdvancedFilters.value) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: InputDecoration(
labelText: 'search'.tr(),
hintText: 'searchPosts'.tr(),
prefixIcon: const Icon(Symbols.search),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
onChanged: (value) {
queryTerm.value = value.isEmpty ? null : value;
},
),
const Gap(12),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'sortBy'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
value: order.value,
items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem(
value: 'popularity',
child: Text('popularity'.tr()),
),
],
onChanged: (value) {
order.value = value;
},
),
const Gap(12),
Row(
children: [
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate:
periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodStart.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'fromDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
const Gap(8),
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate:
periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodEnd.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'toDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
],
),
],
),
),
],
],
),
);
}
}
@riverpod @riverpod
Future<SnPublisher> publisher(Ref ref, String uname) async { Future<SnPublisher> publisher(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
@@ -683,24 +416,22 @@ class PublisherProfileScreen extends HookConsumerWidget {
); );
final categoryTabController = useTabController(initialLength: 3); final categoryTabController = useTabController(initialLength: 3);
final categoryTab = useState(0);
categoryTabController.addListener(() {
categoryTab.value = categoryTabController.index;
});
final includeReplies = useState<bool?>(null); final queryState = useState(PostListQuery(pubName: name));
final mediaOnly = useState(false);
final queryTerm = useState<String?>(null);
final order = useState<String?>('date'); // 'popularity' or 'date'
final orderDesc = useState(
true,
); // true for descending, false for ascending
final periodStart = useState<int?>(null);
final periodEnd = useState<int?>(null);
final showAdvancedFilters = useState(false);
final subscribing = useState(false); final subscribing = useState(false);
final isPinnedExpanded = useState(true); final isPinnedExpanded = useState(true);
useEffect(() {
final index = switch (queryState.value.type) {
0 => 1,
1 => 2,
_ => 0,
};
categoryTabController.index = index;
return null;
}, []);
Future<void> subscribe() async { Future<void> subscribe() async {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
subscribing.value = true; subscribing.value = true;
@@ -739,11 +470,9 @@ class PublisherProfileScreen extends HookConsumerWidget {
); );
return publisher.when( return publisher.when(
data: data: (data) => AppScaffold(
(data) => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: appBar: isWideScreen(context)
isWideScreen(context)
? AppBar( ? AppBar(
foregroundColor: appbarColor.value, foregroundColor: appbarColor.value,
leading: PageBackButton( leading: PageBackButton(
@@ -761,8 +490,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
), ),
) )
: null, : null,
body: body: isWideScreen(context)
isWideScreen(context)
? Row( ? Row(
children: [ children: [
Flexible( Flexible(
@@ -789,51 +517,31 @@ class PublisherProfileScreen extends HookConsumerWidget {
? Symbols.expand_less ? Symbols.expand_less
: Symbols.expand_more, : Symbols.expand_more,
), ),
onTap: onTap: () => isPinnedExpanded.value =
() =>
isPinnedExpanded.value =
!isPinnedExpanded.value, !isPinnedExpanded.value,
), ),
), ),
), ),
...[ ...[
if (isPinnedExpanded.value) if (isPinnedExpanded.value)
SliverPostList(pubName: name, pinned: true), SliverPostList(
query: PostListQuery(pubName: name, pinned: true),
queryKey: 'publisher-$name-pinned',
),
], ],
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherCategoryTabWidget( child: PostFilterWidget(
categoryTabController: categoryTabController, categoryTabController: categoryTabController,
includeReplies: includeReplies, initialQuery: queryState.value,
mediaOnly: mediaOnly, onQueryChanged: (newQuery) =>
queryTerm: queryTerm, queryState.value = newQuery,
order: order,
orderDesc: orderDesc,
periodStart: periodStart,
periodEnd: periodEnd,
showAdvancedFilters: showAdvancedFilters,
), ),
), ),
SliverPostList( SliverPostList(
key: ValueKey( query: queryState.value,
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}', queryKey: 'publisher-$name',
),
pubName: name,
pinned: false,
type:
categoryTab.value == 1
? 0
: (categoryTab.value == 2 ? 1 : null),
includeReplies: includeReplies.value,
mediaOnly: mediaOnly.value,
queryTerm: queryTerm.value,
order: order.value,
orderDesc: orderDesc.value,
periodStart: periodStart.value,
periodEnd: periodEnd.value,
),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
), ),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
], ],
).padding(left: 8), ).padding(left: 8),
), ),
@@ -852,10 +560,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
subscribe: subscribe, subscribe: subscribe,
unsubscribe: unsubscribe, unsubscribe: unsubscribe,
).padding(horizontal: 4, top: 20), ).padding(horizontal: 4, top: 20),
_PublisherBadgesWidget( _PublisherBadgesWidget(data: data, badges: badges),
data: data,
badges: badges,
),
_PublisherVerificationWidget(data: data), _PublisherVerificationWidget(data: data),
_PublisherBioWidget(data: data), _PublisherBioWidget(data: data),
_PublisherHeatmapWidget( _PublisherHeatmapWidget(
@@ -882,14 +587,10 @@ class PublisherProfileScreen extends HookConsumerWidget {
flexibleSpace: Stack( flexibleSpace: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: child: data.background?.id != null
data.background?.id != null ? CloudImageWidget(file: data.background)
? CloudImageWidget(
file: data.background,
)
: Container( : Container(
color: color: Theme.of(
Theme.of(
context, context,
).appBarTheme.backgroundColor, ).appBarTheme.backgroundColor,
), ),
@@ -900,9 +601,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
color: color:
appbarColor.value ?? appbarColor.value ??
Theme.of( Theme.of(context).appBarTheme.foregroundColor,
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow], shadows: [appbarShadow],
), ),
), ),
@@ -922,17 +621,12 @@ class PublisherProfileScreen extends HookConsumerWidget {
).padding(horizontal: 4, top: 8), ).padding(horizontal: 4, top: 8),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherBadgesWidget( child: _PublisherBadgesWidget(data: data, badges: badges),
data: data,
badges: badges,
),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherVerificationWidget(data: data), child: _PublisherVerificationWidget(data: data),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(child: _PublisherBioWidget(data: data)),
child: _PublisherBioWidget(data: data),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherHeatmapWidget( child: _PublisherHeatmapWidget(
heatmap: heatmap, heatmap: heatmap,
@@ -940,10 +634,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Card( child: Card(
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
horizontal: 8,
vertical: 4,
),
child: ListTile( child: ListTile(
title: Text('pinnedPosts'.tr()), title: Text('pinnedPosts'.tr()),
trailing: Icon( trailing: Icon(
@@ -951,60 +642,40 @@ class PublisherProfileScreen extends HookConsumerWidget {
? Symbols.expand_less ? Symbols.expand_less
: Symbols.expand_more, : Symbols.expand_more,
), ),
onTap: onTap: () =>
() => isPinnedExpanded.value = !isPinnedExpanded.value,
isPinnedExpanded.value =
!isPinnedExpanded.value,
), ),
), ),
), ),
...[ ...[
if (isPinnedExpanded.value) if (isPinnedExpanded.value)
SliverPostList(pubName: name, pinned: true), SliverPostList(
query: PostListQuery(pubName: name, pinned: true),
queryKey: 'publisher-$name-pinned',
),
], ],
SliverToBoxAdapter( SliverToBoxAdapter(
child: _PublisherCategoryTabWidget( child: PostFilterWidget(
categoryTabController: categoryTabController, categoryTabController: categoryTabController,
includeReplies: includeReplies, initialQuery: queryState.value,
mediaOnly: mediaOnly, onQueryChanged: (newQuery) => queryState.value = newQuery,
queryTerm: queryTerm,
order: order,
orderDesc: orderDesc,
periodStart: periodStart,
periodEnd: periodEnd,
showAdvancedFilters: showAdvancedFilters,
), ),
), ),
SliverPostList( SliverPostList(
key: ValueKey( key: ValueKey(queryState.value),
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}', query: queryState.value,
), queryKey: 'publisher-$name',
pubName: name,
pinned: false,
type:
categoryTab.value == 1
? 0
: (categoryTab.value == 2 ? 1 : null),
includeReplies: includeReplies.value,
mediaOnly: mediaOnly.value,
queryTerm: queryTerm.value,
order: order.value,
orderDesc: orderDesc.value,
periodStart: periodStart.value,
periodEnd: periodEnd.value,
), ),
SliverGap(MediaQuery.of(context).padding.bottom + 16), SliverGap(MediaQuery.of(context).padding.bottom + 16),
], ],
), ),
), ),
error: error: (error, stackTrace) => AppScaffold(
(error, stackTrace) => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()), appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text(error.toString())), body: Center(child: Text(error.toString())),
), ),
loading: loading: () => AppScaffold(
() => AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()), appBar: AppBar(leading: const PageBackButton()),
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/screens/chat/chat.dart'; import 'package:island/screens/chat/chat.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
@@ -171,11 +172,9 @@ class RealmDetailScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: appBar: isWideScreen(context)
isWideScreen(context)
? realmState.when( ? realmState.when(
data: data: (realm) => AppBar(
(realm) => AppBar(
foregroundColor: appbarColor.value, foregroundColor: appbarColor.value,
leading: PageBackButton( leading: PageBackButton(
color: appbarColor.value, color: appbarColor.value,
@@ -184,14 +183,10 @@ class RealmDetailScreen extends HookConsumerWidget {
flexibleSpace: Stack( flexibleSpace: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: child: realm!.background?.id != null
realm!.background?.id != null ? CloudImageWidget(fileId: realm.background!.id)
? CloudImageWidget(
fileId: realm.background!.id,
)
: Container( : Container(
color: color: Theme.of(
Theme.of(
context, context,
).appBarTheme.backgroundColor, ).appBarTheme.backgroundColor,
), ),
@@ -202,9 +197,7 @@ class RealmDetailScreen extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
color: color:
appbarColor.value ?? appbarColor.value ??
Theme.of( Theme.of(context).appBarTheme.foregroundColor,
context,
).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
@@ -219,16 +212,12 @@ class RealmDetailScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder: (context) =>
(context) =>
_RealmMemberListSheet(realmSlug: slug), _RealmMemberListSheet(realmSlug: slug),
); );
}, },
), ),
_RealmActionMenu( _RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
realmSlug: slug,
iconShadow: iconShadow,
),
const Gap(8), const Gap(8),
], ],
), ),
@@ -239,17 +228,19 @@ class RealmDetailScreen extends HookConsumerWidget {
body: realmState.when( body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')), error: (error, _) => Center(child: Text('Error: $error')),
data: data: (realm) => isWideScreen(context)
(realm) =>
isWideScreen(context)
? Row( ? Row(
children: [ children: [
Flexible( Flexible(
flex: 3, flex: 3,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverPostList(realm: slug, pinned: true), SliverPostList(
SliverPostList(realm: slug, pinned: false), query: PostListQuery(realm: slug, pinned: true),
),
SliverPostList(
query: PostListQuery(realm: slug, pinned: false),
),
], ],
), ),
), ),
@@ -260,14 +251,11 @@ class RealmDetailScreen extends HookConsumerWidget {
realmIdentity.when( realmIdentity.when(
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
data: data: (identity) => Column(
(identity) => Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [ children: [
realmDescriptionWidget(realm!), realmDescriptionWidget(realm!),
if (identity == null && if (identity == null && realm.isCommunity)
realm.isCommunity)
realmActionWidget(realm) realmActionWidget(realm)
else else
const SizedBox.shrink(), const SizedBox.shrink(),
@@ -293,14 +281,10 @@ class RealmDetailScreen extends HookConsumerWidget {
flexibleSpace: Stack( flexibleSpace: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: child: realm!.background?.id != null
realm!.background?.id != null ? CloudImageWidget(fileId: realm.background!.id)
? CloudImageWidget(
fileId: realm.background!.id,
)
: Container( : Container(
color: color: Theme.of(
Theme.of(
context, context,
).appBarTheme.backgroundColor, ).appBarTheme.backgroundColor,
), ),
@@ -311,9 +295,7 @@ class RealmDetailScreen extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
color: color:
appbarColor.value ?? appbarColor.value ??
Theme.of( Theme.of(context).appBarTheme.foregroundColor,
context,
).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
), ),
), ),
@@ -329,17 +311,12 @@ class RealmDetailScreen extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder: (context) =>
(context) => _RealmMemberListSheet( _RealmMemberListSheet(realmSlug: slug),
realmSlug: slug,
),
); );
}, },
), ),
_RealmActionMenu( _RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
realmSlug: slug,
iconShadow: iconShadow,
),
const Gap(8), const Gap(8),
], ],
), ),
@@ -348,10 +325,8 @@ class RealmDetailScreen extends HookConsumerWidget {
child: realmIdentity.when( child: realmIdentity.when(
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
data: data: (identity) => Column(
(identity) => Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [ children: [
realmDescriptionWidget(realm), realmDescriptionWidget(realm),
if (identity == null && realm.isCommunity) if (identity == null && realm.isCommunity)
@@ -362,11 +337,13 @@ class RealmDetailScreen extends HookConsumerWidget {
), ),
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(child: realmChatRoomListWidget(realm)),
child: realmChatRoomListWidget(realm), SliverPostList(
query: PostListQuery(realm: slug, pinned: true),
),
SliverPostList(
query: PostListQuery(realm: slug, pinned: false),
), ),
SliverPostList(realm: slug, pinned: true),
SliverPostList(realm: slug, pinned: false),
], ],
), ),
), ),
@@ -391,8 +368,7 @@ class _RealmActionMenu extends HookConsumerWidget {
return PopupMenuButton( return PopupMenuButton(
icon: Icon(Icons.more_vert, shadows: [iconShadow]), icon: Icon(Icons.more_vert, shadows: [iconShadow]),
itemBuilder: itemBuilder: (context) => [
(context) => [
if (isModerator) if (isModerator)
PopupMenuItem( PopupMenuItem(
onTap: () { onTap: () {
@@ -413,9 +389,7 @@ class _RealmActionMenu extends HookConsumerWidget {
), ),
), ),
realmIdentity.when( realmIdentity.when(
data: data: (identity) => (identity?.role ?? 0) >= 100
(identity) =>
(identity?.role ?? 0) >= 100
? PopupMenuItem( ? PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@@ -478,13 +452,11 @@ class _RealmActionMenu extends HookConsumerWidget {
}); });
}, },
), ),
loading: loading: () => const PopupMenuItem(
() => const PopupMenuItem(
enabled: false, enabled: false,
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
error: error: (_, _) => PopupMenuItem(
(_, _) => PopupMenuItem(
child: Row( child: Row(
children: [ children: [
Icon( Icon(
@@ -494,22 +466,17 @@ class _RealmActionMenu extends HookConsumerWidget {
const Gap(12), const Gap(12),
Text( Text(
'leaveRealm', 'leaveRealm',
style: TextStyle( style: TextStyle(color: Theme.of(context).colorScheme.error),
color: Theme.of(context).colorScheme.error,
),
).tr(), ).tr(),
], ],
), ),
onTap: () { onTap: () {
showConfirmAlert( showConfirmAlert('leaveRealmHint'.tr(), 'leaveRealm'.tr()).then((
'leaveRealmHint'.tr(), confirm,
'leaveRealm'.tr(), ) async {
).then((confirm) async {
if (confirm) { if (confirm) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
await client.delete( await client.delete('/pass/realms/$realmSlug/members/me');
'/pass/realms/$realmSlug/members/me',
);
ref.invalidate(realmsJoinedProvider); ref.invalidate(realmsJoinedProvider);
if (context.mounted) { if (context.mounted) {
context.pop(true); context.pop(true);
@@ -684,8 +651,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: builder: (context) => _RealmMemberRoleSheet(
(context) => _RealmMemberRoleSheet(
realmSlug: realmSlug, realmSlug: realmSlug,
member: member, member: member,
), ),
@@ -809,12 +775,8 @@ class _RealmMemberRoleSheet extends HookConsumerWidget {
onSelected: (int selection) { onSelected: (int selection) {
roleController.text = selection.toString(); roleController.text = selection.toString();
}, },
fieldViewBuilder: ( fieldViewBuilder:
context, (context, controller, focusNode, onFieldSubmitted) {
controller,
focusNode,
onFieldSubmitted,
) {
return TextField( return TextField(
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,

View File

@@ -21,7 +21,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/file_pool.dart'; import 'package:island/pods/drive/file_pool.dart';
class SettingsScreen extends HookConsumerWidget { class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@@ -249,8 +249,7 @@ class SettingsScreen extends HookConsumerWidget {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
Color selectedColor = Color selectedColor = settings.appColorScheme != null
settings.appColorScheme != null
? Color(settings.appColorScheme!) ? Color(settings.appColorScheme!)
: Colors.indigo; : Colors.indigo;
@@ -292,8 +291,7 @@ class SettingsScreen extends HookConsumerWidget {
height: 24, height: 24,
margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8), margin: EdgeInsets.symmetric(horizontal: 2, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: settings.appColorScheme != null
settings.appColorScheme != null
? Color(settings.appColorScheme!) ? Color(settings.appColorScheme!)
: Colors.indigo, : Colors.indigo,
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -310,8 +308,7 @@ class SettingsScreen extends HookConsumerWidget {
// Custom colors section // Custom colors section
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: child: Text(
Text(
'Custom Colors', 'Custom Colors',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
).bold(), ).bold(),
@@ -319,8 +316,7 @@ class SettingsScreen extends HookConsumerWidget {
// Primary color // Primary color
_ColorPickerTile( _ColorPickerTile(
title: 'Primary', title: 'Primary',
color: color: settings.customColors?.primary != null
settings.customColors?.primary != null
? Color(settings.customColors!.primary!) ? Color(settings.customColors!.primary!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -333,8 +329,7 @@ class SettingsScreen extends HookConsumerWidget {
// Secondary // Secondary
_ColorPickerTile( _ColorPickerTile(
title: 'Secondary', title: 'Secondary',
color: color: settings.customColors?.secondary != null
settings.customColors?.secondary != null
? Color(settings.customColors!.secondary!) ? Color(settings.customColors!.secondary!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -347,8 +342,7 @@ class SettingsScreen extends HookConsumerWidget {
// Tertiary // Tertiary
_ColorPickerTile( _ColorPickerTile(
title: 'Tertiary', title: 'Tertiary',
color: color: settings.customColors?.tertiary != null
settings.customColors?.tertiary != null
? Color(settings.customColors!.tertiary!) ? Color(settings.customColors!.tertiary!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -361,8 +355,7 @@ class SettingsScreen extends HookConsumerWidget {
// Surface // Surface
_ColorPickerTile( _ColorPickerTile(
title: 'Surface', title: 'Surface',
color: color: settings.customColors?.surface != null
settings.customColors?.surface != null
? Color(settings.customColors!.surface!) ? Color(settings.customColors!.surface!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -375,8 +368,7 @@ class SettingsScreen extends HookConsumerWidget {
// Background // Background
_ColorPickerTile( _ColorPickerTile(
title: 'Background', title: 'Background',
color: color: settings.customColors?.background != null
settings.customColors?.background != null
? Color(settings.customColors!.background!) ? Color(settings.customColors!.background!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -391,8 +383,7 @@ class SettingsScreen extends HookConsumerWidget {
// Error // Error
_ColorPickerTile( _ColorPickerTile(
title: 'Error', title: 'Error',
color: color: settings.customColors?.error != null
settings.customColors?.error != null
? Color(settings.customColors!.error!) ? Color(settings.customColors!.error!)
: null, : null,
onColorChanged: (color) { onColorChanged: (color) {
@@ -509,8 +500,9 @@ class SettingsScreen extends HookConsumerWidget {
// Background image enabled // Background image enabled
if (!kIsWeb && docBasepath.value != null) if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>( FutureBuilder<bool>(
future: future: File(
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(), '${docBasepath.value}/$kAppBackgroundImagePath',
).exists(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) { if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -536,8 +528,9 @@ class SettingsScreen extends HookConsumerWidget {
// Clear background image option // Clear background image option
if (!kIsWeb && docBasepath.value != null) if (!kIsWeb && docBasepath.value != null)
FutureBuilder<bool>( FutureBuilder<bool>(
future: future: File(
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(), '${docBasepath.value}/$kAppBackgroundImagePath',
).exists(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) { if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -565,8 +558,9 @@ class SettingsScreen extends HookConsumerWidget {
if (!kIsWeb && docBasepath.value != null) if (!kIsWeb && docBasepath.value != null)
FutureBuilder( FutureBuilder(
future: future: File(
File('${docBasepath.value}/$kAppBackgroundImagePath').exists(), '${docBasepath.value}/$kAppBackgroundImagePath',
).exists(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) { if (!snapshot.hasData || !snapshot.data!) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -674,8 +668,7 @@ class SettingsScreen extends HookConsumerWidget {
trailing: DropdownButtonHideUnderline( trailing: DropdownButtonHideUnderline(
child: DropdownButton2<String>( child: DropdownButton2<String>(
isExpanded: true, isExpanded: true,
items: items: validPools.map((p) {
validPools.map((p) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: p.id, value: p.id,
child: Tooltip( child: Tooltip(
@@ -705,14 +698,12 @@ class SettingsScreen extends HookConsumerWidget {
), ),
); );
}, },
loading: loading: () => const ListTile(
() => const ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('Loading pools...'), title: Text('Loading pools...'),
leading: CircularProgressIndicator(), leading: CircularProgressIndicator(),
), ),
error: error: (err, st) => ListTile(
(err, st) => ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('settingsDefaultPool').tr(), title: Text('settingsDefaultPool').tr(),
subtitle: Text('Error: $err'), subtitle: Text('Error: $err'),
@@ -767,8 +758,7 @@ class SettingsScreen extends HookConsumerWidget {
ListTile( ListTile(
minLeadingWidth: 48, minLeadingWidth: 48,
title: Text('settingsEnterToSend').tr(), title: Text('settingsEnterToSend').tr(),
subtitle: subtitle: isDesktop
isDesktop
? Text('settingsEnterToSendDesktopHint').tr().fontSize(12) ? Text('settingsEnterToSendDesktopHint').tr().fontSize(12)
: null, : null,
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
@@ -823,8 +813,7 @@ class SettingsScreen extends HookConsumerWidget {
]; ];
// Desktop-specific settings // Desktop-specific settings
final desktopSettings = final desktopSettings = !isDesktop
!isDesktop
? <Widget>[] ? <Widget>[]
: [ : [
ListTile( ListTile(

View File

@@ -127,13 +127,9 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
// Stickers grid // Stickers grid
Expanded( Expanded(
child: packContent.when( child: packContent.when(
data: data: (stickers) => RefreshIndicator(
(stickers) => RefreshIndicator( onRefresh: () => ref.refresh(
onRefresh: marketplaceStickerPackContentProvider(packId: id).future,
() => ref.refresh(
marketplaceStickerPackContentProvider(
packId: id,
).future,
), ),
child: GridView.builder( child: GridView.builder(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -157,8 +153,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
), ),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.surfaceContainer, ).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
@@ -178,9 +173,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
}, },
), ),
), ),
error: error: (err, _) => Text(
(err, _) =>
Text(
'Error: $err', 'Error: $err',
).textAlignment(TextAlign.center).center(), ).textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(), loading: () => const CircularProgressIndicator().center(),
@@ -189,27 +182,21 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: owned.when( child: owned.when(
data: data: (isOwned) => FilledButton.icon(
(isOwned) => FilledButton.icon( onPressed: isOwned
onPressed:
isOwned
? removePackFromMyCollection ? removePackFromMyCollection
: addPackToMyCollection, : addPackToMyCollection,
icon: Icon( icon: Icon(
isOwned ? Symbols.remove_circle : Symbols.add_circle, isOwned ? Symbols.remove_circle : Symbols.add_circle,
), ),
label: Text( label: Text(isOwned ? 'removePack'.tr() : 'addPack'.tr()),
isOwned ? 'removePack'.tr() : 'addPack'.tr(),
), ),
), loading: () => const SizedBox(
loading:
() => const SizedBox(
height: 32, height: 32,
width: 32, width: 32,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
), ).center(),
error: error: (_, _) => OutlinedButton.icon(
(_, _) => OutlinedButton.icon(
onPressed: addPackToMyCollection, onPressed: addPackToMyCollection,
icon: const Icon(Symbols.add_circle), icon: const Icon(Symbols.add_circle),
label: Text('addPack').tr(), label: Text('addPack').tr(),
@@ -220,8 +207,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
], ],
); );
}, },
error: error: (err, _) =>
(err, _) =>
Text('Error: $err').textAlignment(TextAlign.center).center(), Text('Error: $err').textAlignment(TextAlign.center).center(),
loading: () => const CircularProgressIndicator().center(), loading: () => const CircularProgressIndicator().center(),
), ),

View File

@@ -28,9 +28,8 @@ sealed class MarketplaceStickerQuery with _$MarketplaceStickerQuery {
}) = _MarketplaceStickerQuery; }) = _MarketplaceStickerQuery;
} }
final marketplaceStickerPacksNotifierProvider = AsyncNotifierProvider( final marketplaceStickerPacksNotifierProvider =
MarketplaceStickerPacksNotifier.new, AsyncNotifierProvider.autoDispose(MarketplaceStickerPacksNotifier.new);
);
class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>> class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
with with
@@ -60,8 +59,7 @@ class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final stickers = final stickers = response.data
response.data
.map((e) => SnStickerPack.fromJson(e)) .map((e) => SnStickerPack.fromJson(e))
.cast<SnStickerPack>() .cast<SnStickerPack>()
.toList(); .toList();
@@ -112,12 +110,10 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
onPressed: () { onPressed: () {
query.value = query.value.copyWith(byUsage: !query.value.byUsage); query.value = query.value.copyWith(byUsage: !query.value.byUsage);
}, },
icon: icon: query.value.byUsage
query.value.byUsage
? const Icon(Symbols.local_fire_department) ? const Icon(Symbols.local_fire_department)
: const Icon(Symbols.access_time), : const Icon(Symbols.access_time),
tooltip: tooltip: query.value.byUsage
query.value.byUsage
? 'orderByPopularity'.tr() ? 'orderByPopularity'.tr()
: 'orderByReleaseDate'.tr(), : 'orderByReleaseDate'.tr(),
), ),
@@ -137,8 +133,8 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(
const EdgeInsets.symmetric(horizontal: 24), const EdgeInsets.symmetric(horizontal: 24),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
trailing: [ trailing: [
if (query.value.query != null && query.value.query!.isNotEmpty) if (query.value.query != null && query.value.query!.isNotEmpty)
IconButton( IconButton(
@@ -171,14 +167,13 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
padding: EdgeInsets.only(top: 8), padding: EdgeInsets.only(top: 8),
provider: marketplaceStickerPacksNotifierProvider, provider: marketplaceStickerPacksNotifierProvider,
notifier: marketplaceStickerPacksNotifierProvider.notifier, notifier: marketplaceStickerPacksNotifierProvider.notifier,
itemBuilder: itemBuilder: (context, idx, pack) => Card(
(context, idx, pack) => Card(
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4), margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Column( child: Column(
children: [ children: [
if (pack.stickers.isNotEmpty)
Container( Container(
color: color: Theme.of(context).colorScheme.secondaryContainer,
Theme.of(context).colorScheme.secondaryContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, horizontal: 20,
@@ -200,11 +195,8 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
maxWidth: 80, maxWidth: 80,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(8),
8, color: Theme.of(
),
color:
Theme.of(
context, context,
).colorScheme.tertiaryContainer, ).colorScheme.tertiaryContainer,
), ),
@@ -234,14 +226,12 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
8, 8,
), ),
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.tertiaryContainer, ).colorScheme.tertiaryContainer,
), ),
child: CloudImageWidget( child: CloudImageWidget(
file: file: pack.stickers[index + 4].image,
pack.stickers[index + 4].image,
), ),
).clipRRect(all: 8), ).clipRRect(all: 8),
), ),
@@ -254,8 +244,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
ListTile( ListTile(
leading: Container( leading: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.tertiaryContainer, ).colorScheme.tertiaryContainer,
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
@@ -263,7 +252,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
), ),
), ),
child: CloudImageWidget( child: CloudImageWidget(
file: pack.icon ?? pack.stickers.first.image, file: pack.icon ?? pack.stickers.firstOrNull?.image,
), ),
).width(40).height(40).clipRRect(all: 8), ).width(40).height(40).clipRRect(all: 8),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View File

@@ -112,8 +112,8 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(16), const Gap(16),
@@ -137,8 +137,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
), ),
), ),
items: items: kCurrencyIconData.keys.map((currency) {
kCurrencyIconData.keys.map((currency) {
return DropdownMenuItem( return DropdownMenuItem(
value: currency, value: currency,
child: Row( child: Row(
@@ -178,8 +177,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'enterNumberOfSplits'.tr(), labelText: 'enterNumberOfSplits'.tr(),
hintText: hintText: selectedRecipients.isNotEmpty
selectedRecipients.isNotEmpty
? selectedRecipients.length.toString() ? selectedRecipients.length.toString()
: '1', : '1',
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -188,12 +186,12 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) { onChanged: (value) {
if (value.isEmpty && selectedRecipients.isNotEmpty) { if (value.isEmpty && selectedRecipients.isNotEmpty) {
splitsController.text = splitsController.text = selectedRecipients.length
selectedRecipients.length.toString(); .toString();
} }
}, },
), ),
@@ -261,8 +259,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
).colorScheme.outline.withOpacity(0.2), ).colorScheme.outline.withOpacity(0.2),
), ),
), ),
child: child: selectedRecipients.isNotEmpty
selectedRecipients.isNotEmpty
? Column( ? Column(
children: [ children: [
...selectedRecipients.map((recipient) { ...selectedRecipients.map((recipient) {
@@ -283,26 +280,23 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
subtitle: Text( subtitle: Text(
'selectedRecipient'.tr(), 'selectedRecipient'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodySmall
context, ?.copyWith(
).textTheme.bodySmall?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
), ),
trailing: IconButton( trailing: IconButton(
onPressed: onPressed: () => setState(
() => setState( () =>
() => selectedRecipients.remove( selectedRecipients.remove(recipient),
recipient,
),
), ),
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
color: color: Theme.of(
Theme.of(context).colorScheme.error, context,
).colorScheme.error,
), ),
tooltip: 'Remove recipient', tooltip: 'Remove recipient',
), ),
@@ -316,19 +310,16 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
Icon( Icon(
Icons.person_add_outlined, Icons.person_add_outlined,
size: 48, size: 48,
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
const Gap(8), const Gap(8),
Text( Text(
'noRecipientsSelected'.tr(), 'noRecipientsSelected'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodyMedium
context, ?.copyWith(
).textTheme.bodyMedium?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
@@ -336,11 +327,9 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
const Gap(4), const Gap(4),
Text( Text(
'selectRecipientsToSendFund'.tr(), 'selectRecipientsToSendFund'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodySmall
context, ?.copyWith(
).textTheme.bodySmall?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
@@ -399,8 +388,8 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
), ),
), ),
maxLines: 3, maxLines: 3,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@@ -441,13 +430,10 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
useSafeArea: true, useSafeArea: true,
builder: builder: (context) => Container(
(context) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
top: Radius.circular(16),
),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -473,10 +459,10 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
const Gap(24), const Gap(24),
OtpTextField( OtpTextField(
numberOfFields: 6, numberOfFields: 6,
borderColor: borderColor: Theme.of(context).colorScheme.outline,
Theme.of(context).colorScheme.outline, focusedBorderColor: Theme.of(
focusedBorderColor: context,
Theme.of(context).colorScheme.primary, ).colorScheme.primary,
showFieldAsBox: true, showFieldAsBox: true,
obscureText: true, obscureText: true,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -484,9 +470,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
fieldHeight: 56, fieldHeight: 56,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderWidth: 1, borderWidth: 1,
textStyle: Theme.of(context) textStyle: Theme.of(context).textTheme.headlineSmall
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
onSubmit: (pin) { onSubmit: (pin) {
enteredPin = pin; enteredPin = pin;
@@ -552,8 +536,7 @@ class _CreateFundSheetState extends State<CreateFundSheet> {
'split_type': selectedSplitType, 'split_type': selectedSplitType,
'amount_of_splits': splits, 'amount_of_splits': splits,
'recipient_account_ids': selectedRecipients.map((r) => r.id).toList(), 'recipient_account_ids': selectedRecipients.map((r) => r.id).toList(),
'message': 'message': messageController.text.trim().isEmpty
messageController.text.trim().isEmpty
? null ? null
: messageController.text.trim(), : messageController.text.trim(),
'pin_code': '', // Will be filled by PIN verification 'pin_code': '', // Will be filled by PIN verification
@@ -632,8 +615,8 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
), ),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(16), const Gap(16),
@@ -657,8 +640,7 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
), ),
), ),
), ),
items: items: kCurrencyIconData.keys.map((currency) {
kCurrencyIconData.keys.map((currency) {
return DropdownMenuItem( return DropdownMenuItem(
value: currency, value: currency,
child: Row( child: Row(
@@ -702,8 +684,7 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
).colorScheme.outline.withOpacity(0.2), ).colorScheme.outline.withOpacity(0.2),
), ),
), ),
child: child: selectedPayee != null
selectedPayee != null
? ListTile( ? ListTile(
contentPadding: const EdgeInsets.only( contentPadding: const EdgeInsets.only(
left: 20, left: 20,
@@ -721,18 +702,16 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
), ),
subtitle: Text( subtitle: Text(
'selectedPayee'.tr(), 'selectedPayee'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodySmall
context, ?.copyWith(
).textTheme.bodySmall?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
), ),
trailing: IconButton( trailing: IconButton(
onPressed: onPressed: () =>
() => setState(() => selectedPayee = null), setState(() => selectedPayee = null),
icon: Icon( icon: Icon(
Icons.clear, Icons.clear,
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
@@ -746,19 +725,16 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
Icon( Icon(
Icons.person_add_outlined, Icons.person_add_outlined,
size: 48, size: 48,
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
const Gap(8), const Gap(8),
Text( Text(
'noPayeeSelected'.tr(), 'noPayeeSelected'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodyMedium
context, ?.copyWith(
).textTheme.bodyMedium?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
@@ -766,11 +742,9 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
const Gap(4), const Gap(4),
Text( Text(
'selectPayeeToTransfer'.tr(), 'selectPayeeToTransfer'.tr(),
style: Theme.of( style: Theme.of(context).textTheme.bodySmall
context, ?.copyWith(
).textTheme.bodySmall?.copyWith( color: Theme.of(
color:
Theme.of(
context, context,
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
@@ -824,8 +798,8 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
), ),
), ),
maxLines: 3, maxLines: 3,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
], ],
), ),
@@ -866,13 +840,10 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
useSafeArea: true, useSafeArea: true,
builder: builder: (context) => Container(
(context) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
top: Radius.circular(16),
),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -898,10 +869,10 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
const Gap(24), const Gap(24),
OtpTextField( OtpTextField(
numberOfFields: 6, numberOfFields: 6,
borderColor: borderColor: Theme.of(context).colorScheme.outline,
Theme.of(context).colorScheme.outline, focusedBorderColor: Theme.of(
focusedBorderColor: context,
Theme.of(context).colorScheme.primary, ).colorScheme.primary,
showFieldAsBox: true, showFieldAsBox: true,
obscureText: true, obscureText: true,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -909,9 +880,7 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
fieldHeight: 56, fieldHeight: 56,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderWidth: 1, borderWidth: 1,
textStyle: Theme.of(context) textStyle: Theme.of(context).textTheme.headlineSmall
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
onSubmit: (pin) { onSubmit: (pin) {
enteredPin = pin; enteredPin = pin;
@@ -974,8 +943,7 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
'amount': amount, 'amount': amount,
'currency': selectedCurrency, 'currency': selectedCurrency,
'payee_account_id': selectedPayee!.id, 'payee_account_id': selectedPayee!.id,
'remark': 'remark': remarkController.text.trim().isEmpty
remarkController.text.trim().isEmpty
? null ? null
: remarkController.text.trim(), : remarkController.text.trim(),
}; };
@@ -991,7 +959,7 @@ class _CreateTransferSheetState extends State<CreateTransferSheet> {
} }
} }
final transactionListProvider = AsyncNotifierProvider( final transactionListProvider = AsyncNotifierProvider.autoDispose(
TransactionListNotifier.new, TransactionListNotifier.new,
); );
@@ -1012,14 +980,17 @@ class TransactionListNotifier extends AsyncNotifier<List<SnTransaction>>
); );
totalCount = int.parse(response.headers.value('X-Total') ?? '0'); totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data; final List<dynamic> data = response.data;
final transactions = final transactions = data
data.map((json) => SnTransaction.fromJson(json)).toList(); .map((json) => SnTransaction.fromJson(json))
.toList();
return transactions; return transactions;
} }
} }
final walletFundsProvider = AsyncNotifierProvider(WalletFundsNotifier.new); final walletFundsProvider = AsyncNotifierProvider.autoDispose(
WalletFundsNotifier.new,
);
class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>> class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>>
with AsyncPaginationController<SnWalletFund> { with AsyncPaginationController<SnWalletFund> {
@@ -1034,8 +1005,9 @@ class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>>
'/pass/wallets/funds?offset=$offset&take=$pageSize', '/pass/wallets/funds?offset=$offset&take=$pageSize',
); );
// Assuming total count header is present or we just check if list is empty // Assuming total count header is present or we just check if list is empty
final list = final list = (response.data as List)
(response.data as List).map((e) => SnWalletFund.fromJson(e)).toList(); .map((e) => SnWalletFund.fromJson(e))
.toList();
if (list.length < pageSize) { if (list.length < pageSize) {
totalCount = fetchedCount + list.length; totalCount = fetchedCount + list.length;
} }
@@ -1043,7 +1015,7 @@ class WalletFundsNotifier extends AsyncNotifier<List<SnWalletFund>>
} }
} }
final walletFundRecipientsProvider = AsyncNotifierProvider( final walletFundRecipientsProvider = AsyncNotifierProvider.autoDispose(
WalletFundRecipientsNotifier.new, WalletFundRecipientsNotifier.new,
); );
@@ -1060,8 +1032,7 @@ class WalletFundRecipientsNotifier
final response = await client.get( final response = await client.get(
'/pass/wallets/funds/recipients?offset=$offset&take=$_pageSize', '/pass/wallets/funds/recipients?offset=$offset&take=$_pageSize',
); );
final list = final list = (response.data as List)
(response.data as List)
.map((e) => SnWalletFundRecipient.fromJson(e)) .map((e) => SnWalletFundRecipient.fromJson(e))
.toList(); .toList();
@@ -1312,8 +1283,7 @@ class WalletScreen extends HookConsumerWidget {
return allCurrencies.map((currency) { return allCurrencies.map((currency) {
final existingPocket = pockets.firstWhere( final existingPocket = pockets.firstWhere(
(p) => p.currency == currency, (p) => p.currency == currency,
orElse: orElse: () => SnWalletPocket(
() => SnWalletPocket(
id: '', id: '',
currency: currency, currency: currency,
amount: 0.0, amount: 0.0,
@@ -1338,10 +1308,10 @@ class WalletScreen extends HookConsumerWidget {
? Symbols.money_bag ? Symbols.money_bag
: Symbols.swap_horiz, : Symbols.swap_horiz,
), ),
onPressed: onPressed: currentTabIndex.value == 1
currentTabIndex.value == 1 ? createFund : createTransfer, ? createFund
tooltip: : createTransfer,
currentTabIndex.value == 1 tooltip: currentTabIndex.value == 1
? 'createFund'.tr() ? 'createFund'.tr()
: 'createTransfer'.tr(), : 'createTransfer'.tr(),
), ),
@@ -1368,8 +1338,7 @@ class WalletScreen extends HookConsumerWidget {
} }
return NestedScrollView( return NestedScrollView(
headerSliverBuilder: headerSliverBuilder: (context, innerBoxIsScrolled) => [
(context, innerBoxIsScrolled) => [
// Wallet Overview // Wallet Overview
SliverToBoxAdapter( SliverToBoxAdapter(
child: Column( child: Column(
@@ -1388,11 +1357,8 @@ class WalletScreen extends HookConsumerWidget {
kCurrencyIconData[pocket.currency] ?? kCurrencyIconData[pocket.currency] ??
Symbols.universal_currency_alt, Symbols.universal_currency_alt,
), ),
title: title: Text(
Text( getCurrencyTranslationKey(pocket.currency),
getCurrencyTranslationKey(
pocket.currency,
),
).tr(), ).tr(),
subtitle: Text( subtitle: Text(
'${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}',
@@ -1438,10 +1404,8 @@ class WalletScreen extends HookConsumerWidget {
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) =>
(context) => TransactionDetailSheet( TransactionDetailSheet(transaction: transaction),
transaction: transaction,
),
); );
}, },
child: ListTile( child: ListTile(
@@ -1479,8 +1443,7 @@ class WalletScreen extends HookConsumerWidget {
), ),
); );
}, },
error: error: (error, stackTrace) => ResponseErrorWidget(
(error, stackTrace) => ResponseErrorWidget(
error: error, error: error,
onRetry: () => ref.invalidate(walletCurrentProvider), onRetry: () => ref.invalidate(walletCurrentProvider),
), ),
@@ -1554,8 +1517,9 @@ class WalletScreen extends HookConsumerWidget {
itemCount: fundList.length, itemCount: fundList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final fund = fundList[index]; final fund = fundList[index];
final claimedCount = final claimedCount = fund.recipients
fund.recipients.where((r) => r.isReceived).length; .where((r) => r.isReceived)
.length;
final totalRecipients = fund.recipients.length; final totalRecipients = fund.recipients.length;
return Card( return Card(
@@ -1724,16 +1688,14 @@ class WalletScreen extends HookConsumerWidget {
), ),
); );
}, },
loading: loading: () => Card(
() => Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: const Padding( child: const Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
), ),
error: error: (error, stack) => Card(
(error, stack) => Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),

View File

@@ -7,7 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/upload_tasks.dart'; import 'package:island/pods/drive/upload_tasks.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:native_exif/native_exif.dart'; import 'package:native_exif/native_exif.dart';
import 'package:path/path.dart' show extension; import 'package:path/path.dart' show extension;
@@ -211,8 +211,9 @@ class FileUploader {
// Use old way for Uint8List // Use old way for Uint8List
final chunks = <Uint8List>[]; final chunks = <Uint8List>[];
for (int i = 0; i < fileData.length; i += chunkSize) { for (int i = 0; i < fileData.length; i += chunkSize) {
final end = final end = i + chunkSize > fileData.length
i + chunkSize > fileData.length ? fileData.length : i + chunkSize; ? fileData.length
: i + chunkSize;
chunks.add(Uint8List.fromList(fileData.sublist(i, end))); chunks.add(Uint8List.fromList(fileData.sublist(i, end)));
} }

View File

@@ -1 +0,0 @@

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/file_pool.dart'; import 'package:island/models/file_pool.dart';
import 'package:island/pods/file_pool.dart'; import 'package:island/pods/drive/file_pool.dart';
import 'package:island/widgets/content/attachment_preview.dart'; import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/compose_shared.dart';
@@ -79,8 +79,7 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
children: [ children: [
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedPoolId, value: selectedPoolId,
items: items: pools.map((pool) {
pools.map((pool) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: pool.id, value: pool.id,
child: Text(pool.name), child: Text(pool.name),
@@ -140,8 +139,7 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.errorContainer, ).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@@ -155,19 +153,18 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
Icon( Icon(
Symbols.warning, Symbols.warning,
size: 18, size: 18,
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.error, ).colorScheme.error,
), ),
const Gap(8), const Gap(8),
Text( Text(
'uploadConstraints'.tr(), 'uploadConstraints'.tr(),
style: Theme.of( style: Theme.of(context)
context, .textTheme
).textTheme.bodyMedium?.copyWith( .bodyMedium
color: ?.copyWith(
Theme.of( color: Theme.of(
context, context,
).colorScheme.error, ).colorScheme.error,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -183,11 +180,11 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
_formatFileSize(maxFileSize), _formatFileSize(maxFileSize),
], ],
), ),
style: Theme.of( style: Theme.of(context)
context, .textTheme
).textTheme.bodySmall?.copyWith( .bodySmall
color: ?.copyWith(
Theme.of( color: Theme.of(
context, context,
).colorScheme.error, ).colorScheme.error,
), ),
@@ -197,11 +194,11 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
const Gap(4), const Gap(4),
Text( Text(
'fileTypeNotAccepted'.tr(), 'fileTypeNotAccepted'.tr(),
style: Theme.of( style: Theme.of(context)
context, .textTheme
).textTheme.bodySmall?.copyWith( .bodySmall
color: ?.copyWith(
Theme.of( color: Theme.of(
context, context,
).colorScheme.error, ).colorScheme.error,
), ),
@@ -229,8 +226,7 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
), ),
], ],
), ),
style: style: Theme.of(
Theme.of(
context, context,
).textTheme.bodyMedium, ).textTheme.bodyMedium,
).fontSize(13), ).fontSize(13),
@@ -300,8 +296,8 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?; final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize; final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
final acceptTypes = final acceptTypes = (selectedPool.policyConfig?['accept_types'] as List?)
(selectedPool.policyConfig?['accept_types'] as List?)?.cast<String>(); ?.cast<String>();
final mimeType = final mimeType =
attachment.data.mimeType ?? attachment.data.mimeType ??
ComposeLogic.getMimeTypeFromFileType(attachment.type); ComposeLogic.getMimeTypeFromFileType(attachment.type);

View File

@@ -9,8 +9,11 @@ import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/utils/format.dart'; import 'package:island/utils/format.dart';
import 'package:island/widgets/content/profile_decoration.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:island/widgets/data_saving_gate.dart'; import 'package:island/widgets/data_saving_gate.dart';
import 'file_viewer_contents.dart'; import 'file_viewer_contents.dart';
@@ -258,15 +261,13 @@ class CloudFileWidget extends HookConsumerWidget {
var content = switch (item.mimeType?.split('/').firstOrNull) { var content = switch (item.mimeType?.split('/').firstOrNull) {
'image' => AspectRatio( 'image' => AspectRatio(
aspectRatio: ratio, aspectRatio: ratio,
child: child: (useInternalGate && dataSaving && !unlocked.value)
(useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.image) ? dataPlaceHolder(Symbols.image)
: cloudImage(), : cloudImage(),
), ),
'video' => AspectRatio( 'video' => AspectRatio(
aspectRatio: ratio, aspectRatio: ratio,
child: child: (useInternalGate && dataSaving && !unlocked.value)
(useInternalGate && dataSaving && !unlocked.value)
? dataPlaceHolder(Symbols.play_arrow) ? dataPlaceHolder(Symbols.play_arrow)
: cloudVideo(), : cloudVideo(),
), ),
@@ -383,8 +384,7 @@ class CloudVideoWidget extends HookConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/${item.id}'; final uri = '$serverUrl/drive/files/${item.id}';
var ratio = var ratio = item.fileMeta?['ratio'] is num
item.fileMeta?['ratio'] is num
? item.fileMeta!['ratio'].toDouble() ? item.fileMeta!['ratio'].toDouble()
: 1.0; : 1.0;
if (ratio == 0) ratio = 1.0; if (ratio == 0) ratio = 1.0;
@@ -533,8 +533,7 @@ class CloudImageWidget extends ConsumerWidget {
return AspectRatio( return AspectRatio(
aspectRatio: aspectRatio, aspectRatio: aspectRatio,
child: child: file != null
file != null
? CloudFileWidget(item: file!, fit: fit) ? CloudFileWidget(item: file!, fit: fit)
: UniversalImage(uri: uri, blurHash: blurHash, fit: fit), : UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
); );
@@ -545,8 +544,7 @@ class CloudImageWidget extends ConsumerWidget {
required String serverUrl, required String serverUrl,
bool original = false, bool original = false,
}) { }) {
final uri = final uri = original
original
? '$serverUrl/drive/files/$fileId?original=true' ? '$serverUrl/drive/files/$fileId?original=true'
: '$serverUrl/drive/files/$fileId'; : '$serverUrl/drive/files/$fileId';
return CachedNetworkImageProvider(uri); return CachedNetworkImageProvider(uri);
@@ -560,6 +558,7 @@ class ProfilePictureWidget extends ConsumerWidget {
final double? borderRadius; final double? borderRadius;
final IconData? fallbackIcon; final IconData? fallbackIcon;
final Color? fallbackColor; final Color? fallbackColor;
final ProfileDecoration? decoration;
const ProfilePictureWidget({ const ProfilePictureWidget({
super.key, super.key,
this.fileId, this.fileId,
@@ -568,6 +567,7 @@ class ProfilePictureWidget extends ConsumerWidget {
this.borderRadius, this.borderRadius,
this.fallbackIcon, this.fallbackIcon,
this.fallbackColor, this.fallbackColor,
this.decoration,
}); });
@override @override
@@ -575,36 +575,49 @@ class ProfilePictureWidget extends ConsumerWidget {
final serverUrl = ref.watch(serverUrlProvider); final serverUrl = ref.watch(serverUrlProvider);
final String? id = file?.id ?? fileId; final String? id = file?.id ?? fileId;
final fallback = final fallback = Icon(
Icon(
fallbackIcon ?? Symbols.account_circle, fallbackIcon ?? Symbols.account_circle,
size: radius, size: radius,
color: color: fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
).center(); ).center();
return ClipRRect( final image = id == null
borderRadius:
borderRadius == null
? BorderRadius.all(Radius.circular(radius))
: BorderRadius.all(Radius.circular(borderRadius!)),
child: Container(
width: radius * 2,
height: radius * 2,
color: Theme.of(context).colorScheme.primaryContainer,
child:
id == null
? fallback ? fallback
: DataSavingGate( : DataSavingGate(
bypass: true, bypass: true,
placeholder: fallback, placeholder: fallback,
content: content: () => UniversalImage(
() => UniversalImage(
uri: '$serverUrl/drive/files/$id', uri: '$serverUrl/drive/files/$id',
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
);
Widget content = Container(
width: radius * 2,
height: radius * 2,
color: Theme.of(context).colorScheme.primaryContainer,
child: decoration != null
? Stack(
fit: StackFit.expand,
children: [
image,
CustomPaint(
painter: _ProfileDecorationPainter(
text: decoration!.text,
color: decoration!.color,
textColor: decoration!.textColor ?? Colors.white,
), ),
), ),
],
)
: image,
);
return ClipRRect(
borderRadius: borderRadius == null
? BorderRadius.all(Radius.circular(radius))
: BorderRadius.all(Radius.circular(borderRadius!)),
child: content,
); );
} }
} }
@@ -716,11 +729,9 @@ class SplitAvatarWidget extends ConsumerWidget {
), ),
), ),
Expanded( Expanded(
child: child: filesId.length > 4
filesId.length > 4
? Container( ? Container(
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.primaryContainer, ).colorScheme.primaryContainer,
child: Center( child: Center(
@@ -728,8 +739,7 @@ class SplitAvatarWidget extends ConsumerWidget {
'+${filesId.length - 3}', '+${filesId.length - 3}',
style: TextStyle( style: TextStyle(
fontSize: radius * 0.4, fontSize: radius * 0.4,
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.onPrimaryContainer, ).colorScheme.onPrimaryContainer,
), ),
@@ -765,13 +775,11 @@ class SplitAvatarWidget extends ConsumerWidget {
width: radius, width: radius,
height: radius, height: radius,
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
child: child: Icon(
Icon(
fallbackIcon, fallbackIcon,
size: radius * 0.6, size: radius * 0.6,
color: color:
fallbackColor ?? fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
Theme.of(context).colorScheme.onPrimaryContainer,
).center(), ).center(),
); );
} }
@@ -786,3 +794,106 @@ class SplitAvatarWidget extends ConsumerWidget {
); );
} }
} }
class _ProfileDecorationPainter extends CustomPainter {
final String text;
final Color color;
final Color textColor;
_ProfileDecorationPainter({
required this.text,
required this.color,
required this.textColor,
});
@override
void paint(Canvas canvas, Size size) {
if (text.isEmpty) return;
final radius = size.width / 2;
final center = Offset(size.width / 2, size.height / 2);
final strokeWidth = radius * 0.4; // Increased thickness
final centerAngle = 3 * math.pi / 4;
final sweepAngle = math.pi / 1;
final startAngle = centerAngle - (sweepAngle / 2);
final arcRadius = radius - (strokeWidth / 2);
final rect = Rect.fromCircle(center: center, radius: arcRadius);
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..shader = SweepGradient(
startAngle: startAngle,
endAngle: startAngle + sweepAngle,
colors: [color.withOpacity(0), color, color, color.withOpacity(0)],
stops: const [0.0, 0.25, 0.75, 1.0],
).createShader(rect);
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
_drawTextOnArc(canvas, center, arcRadius, text, centerAngle);
}
void _drawTextOnArc(
Canvas canvas,
Offset center,
double radius,
String text,
double centerAngle,
) {
final textStyle = TextStyle(
color: textColor,
fontSize: radius * 0.28,
fontWeight: FontWeight.bold,
);
double totalAngle = 0;
List<double> charAngles = [];
// Calculate total angle occupied by text
for (int i = 0; i < text.length; i++) {
final char = text[i];
final span = TextSpan(text: char, style: textStyle);
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
tp.layout();
final charWidth = tp.width;
final angle = charWidth / radius;
charAngles.add(angle);
totalAngle += angle;
}
// Start from "Left" of the center (High angle)
// We want to traverse from centerAngle + total/2 to centerAngle - total/2
double currentAngle = centerAngle + (totalAngle / 2);
for (int i = 0; i < text.length; i++) {
final char = text[i];
final span = TextSpan(text: char, style: textStyle);
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
tp.layout();
final charAngle = charAngles[i];
final midCharAngle = currentAngle - charAngle / 2;
final x = center.dx + radius * math.cos(midCharAngle);
final y = center.dy + radius * math.sin(midCharAngle);
canvas.save();
canvas.translate(x, y);
canvas.rotate(midCharAngle - math.pi / 2);
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
canvas.restore();
currentAngle -= charAngle;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'profile_decoration.freezed.dart';
@freezed
sealed class ProfileDecoration with _$ProfileDecoration {
const factory ProfileDecoration({
required String text,
required Color color,
Color? textColor,
}) = _ProfileDecoration;
}

View File

@@ -0,0 +1,277 @@
// 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 'profile_decoration.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProfileDecoration {
String get text; Color get color; Color? get textColor;
/// Create a copy of ProfileDecoration
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ProfileDecorationCopyWith<ProfileDecoration> get copyWith => _$ProfileDecorationCopyWithImpl<ProfileDecoration>(this as ProfileDecoration, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileDecoration&&(identical(other.text, text) || other.text == text)&&(identical(other.color, color) || other.color == color)&&(identical(other.textColor, textColor) || other.textColor == textColor));
}
@override
int get hashCode => Object.hash(runtimeType,text,color,textColor);
@override
String toString() {
return 'ProfileDecoration(text: $text, color: $color, textColor: $textColor)';
}
}
/// @nodoc
abstract mixin class $ProfileDecorationCopyWith<$Res> {
factory $ProfileDecorationCopyWith(ProfileDecoration value, $Res Function(ProfileDecoration) _then) = _$ProfileDecorationCopyWithImpl;
@useResult
$Res call({
String text, Color color, Color? textColor
});
}
/// @nodoc
class _$ProfileDecorationCopyWithImpl<$Res>
implements $ProfileDecorationCopyWith<$Res> {
_$ProfileDecorationCopyWithImpl(this._self, this._then);
final ProfileDecoration _self;
final $Res Function(ProfileDecoration) _then;
/// Create a copy of ProfileDecoration
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? color = null,Object? textColor = freezed,}) {
return _then(_self.copyWith(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,color: null == color ? _self.color : color // ignore: cast_nullable_to_non_nullable
as Color,textColor: freezed == textColor ? _self.textColor : textColor // ignore: cast_nullable_to_non_nullable
as Color?,
));
}
}
/// Adds pattern-matching-related methods to [ProfileDecoration].
extension ProfileDecorationPatterns on ProfileDecoration {
/// 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( _ProfileDecoration value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ProfileDecoration() 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( _ProfileDecoration value) $default,){
final _that = this;
switch (_that) {
case _ProfileDecoration():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// 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( _ProfileDecoration value)? $default,){
final _that = this;
switch (_that) {
case _ProfileDecoration() 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 text, Color color, Color? textColor)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProfileDecoration() when $default != null:
return $default(_that.text,_that.color,_that.textColor);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 text, Color color, Color? textColor) $default,) {final _that = this;
switch (_that) {
case _ProfileDecoration():
return $default(_that.text,_that.color,_that.textColor);case _:
throw StateError('Unexpected subclass');
}
}
/// 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 text, Color color, Color? textColor)? $default,) {final _that = this;
switch (_that) {
case _ProfileDecoration() when $default != null:
return $default(_that.text,_that.color,_that.textColor);case _:
return null;
}
}
}
/// @nodoc
class _ProfileDecoration implements ProfileDecoration {
const _ProfileDecoration({required this.text, required this.color, this.textColor});
@override final String text;
@override final Color color;
@override final Color? textColor;
/// Create a copy of ProfileDecoration
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ProfileDecorationCopyWith<_ProfileDecoration> get copyWith => __$ProfileDecorationCopyWithImpl<_ProfileDecoration>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileDecoration&&(identical(other.text, text) || other.text == text)&&(identical(other.color, color) || other.color == color)&&(identical(other.textColor, textColor) || other.textColor == textColor));
}
@override
int get hashCode => Object.hash(runtimeType,text,color,textColor);
@override
String toString() {
return 'ProfileDecoration(text: $text, color: $color, textColor: $textColor)';
}
}
/// @nodoc
abstract mixin class _$ProfileDecorationCopyWith<$Res> implements $ProfileDecorationCopyWith<$Res> {
factory _$ProfileDecorationCopyWith(_ProfileDecoration value, $Res Function(_ProfileDecoration) _then) = __$ProfileDecorationCopyWithImpl;
@override @useResult
$Res call({
String text, Color color, Color? textColor
});
}
/// @nodoc
class __$ProfileDecorationCopyWithImpl<$Res>
implements _$ProfileDecorationCopyWith<$Res> {
__$ProfileDecorationCopyWithImpl(this._self, this._then);
final _ProfileDecoration _self;
final $Res Function(_ProfileDecoration) _then;
/// Create a copy of ProfileDecoration
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? color = null,Object? textColor = freezed,}) {
return _then(_ProfileDecoration(
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
as String,color: null == color ? _self.color : color // ignore: cast_nullable_to_non_nullable
as Color,textColor: freezed == textColor ? _self.textColor : textColor // ignore: cast_nullable_to_non_nullable
as Color?,
));
}
}
// dart format on

View File

@@ -12,8 +12,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file_list_item.dart'; import 'package:island/models/file_list_item.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/file_pool.dart'; import 'package:island/models/file_pool.dart';
import 'package:island/pods/file_list.dart'; import 'package:island/pods/drive/file_list.dart';
import 'package:island/pods/file_pool.dart'; import 'package:island/pods/drive/file_pool.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/file_uploader.dart'; import 'package:island/services/file_uploader.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
@@ -123,12 +123,8 @@ class FileListView extends HookConsumerWidget {
notifier: unindexedFileListProvider.notifier, notifier: unindexedFileListProvider.notifier,
isRefreshable: false, isRefreshable: false,
isSliver: true, isSliver: true,
contentBuilder: contentBuilder: (data, footer) => data.isEmpty
(data, footer) => ? SliverToBoxAdapter(child: _buildEmptyUnindexedFilesHint(ref))
data.isEmpty
? SliverToBoxAdapter(
child: _buildEmptyUnindexedFilesHint(ref),
)
: _buildUnindexedFileListContent( : _buildUnindexedFileListContent(
data, data,
ref, ref,
@@ -145,9 +141,7 @@ class FileListView extends HookConsumerWidget {
notifier: indexedCloudFileListProvider.notifier, notifier: indexedCloudFileListProvider.notifier,
isRefreshable: false, isRefreshable: false,
isSliver: true, isSliver: true,
contentBuilder: contentBuilder: (data, footer) => data.isEmpty
(data, footer) =>
data.isEmpty
? SliverToBoxAdapter( ? SliverToBoxAdapter(
child: _buildEmptyDirectoryHint(ref, currentPath), child: _buildEmptyDirectoryHint(ref, currentPath),
) )
@@ -177,8 +171,7 @@ class FileListView extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
); );
} else { } else {
final pathParts = final pathParts = currentPath.value
currentPath.value
.split('/') .split('/')
.where((part) => part.isNotEmpty) .where((part) => part.isNotEmpty)
.toList(); .toList();
@@ -266,8 +259,7 @@ class FileListView extends HookConsumerWidget {
dragging.value = false; dragging.value = false;
}, },
child: Container( child: Container(
color: color: dragging.value
dragging.value
? Theme.of(context).primaryColor.withOpacity(0.1) ? Theme.of(context).primaryColor.withOpacity(0.1)
: null, : null,
child: Column( child: Column(
@@ -302,23 +294,20 @@ class FileListView extends HookConsumerWidget {
? Symbols.arrow_back ? Symbols.arrow_back
: Symbols.folder, : Symbols.folder,
), ),
onPressed: onPressed: isRefreshing
isRefreshing
? null ? null
: () { : () {
if (mode.value == FileListMode.unindexed) { if (mode.value == FileListMode.unindexed) {
mode.value = FileListMode.normal; mode.value = FileListMode.normal;
currentPath.value = '/'; currentPath.value = '/';
} else { } else {
final pathParts = final pathParts = currentPath.value
currentPath.value
.split('/') .split('/')
.where((part) => part.isNotEmpty) .where((part) => part.isNotEmpty)
.toList(); .toList();
if (pathParts.isNotEmpty) { if (pathParts.isNotEmpty) {
pathParts.removeLast(); pathParts.removeLast();
currentPath.value = currentPath.value = pathParts.isEmpty
pathParts.isEmpty
? '/' ? '/'
: '/${pathParts.join('/')}'; : '/${pathParts.join('/')}';
} }
@@ -342,14 +331,11 @@ class FileListView extends HookConsumerWidget {
? Symbols.view_module ? Symbols.view_module
: Symbols.list, : Symbols.list,
), ),
onPressed: onPressed: () => viewMode.value =
() =>
viewMode.value =
viewMode.value == FileListViewMode.list viewMode.value == FileListViewMode.list
? FileListViewMode.waterfall ? FileListViewMode.waterfall
: FileListViewMode.list, : FileListViewMode.list,
tooltip: tooltip: viewMode.value == FileListViewMode.list
viewMode.value == FileListViewMode.list
? 'Switch to Waterfall View' ? 'Switch to Waterfall View'
: 'Switch to List View', : 'Switch to List View',
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
@@ -363,10 +349,9 @@ class FileListView extends HookConsumerWidget {
? Symbols.close ? Symbols.close
: Symbols.select_check_box, : Symbols.select_check_box,
), ),
onPressed: onPressed: () =>
() => isSelectionMode.value = !isSelectionMode.value, isSelectionMode.value = !isSelectionMode.value,
tooltip: tooltip: isSelectionMode.value
isSelectionMode.value
? 'Exit Selection Mode' ? 'Exit Selection Mode'
: 'Enter Selection Mode', : 'Enter Selection Mode',
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
@@ -377,8 +362,7 @@ class FileListView extends HookConsumerWidget {
if (mode.value == FileListMode.normal) if (mode.value == FileListMode.normal)
IconButton( IconButton(
icon: const Icon(Symbols.create_new_folder), icon: const Icon(Symbols.create_new_folder),
onPressed: onPressed: () =>
() =>
onShowCreateDirectory(ref.context, currentPath), onShowCreateDirectory(ref.context, currentPath),
tooltip: 'Create Directory', tooltip: 'Create Directory',
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
@@ -397,8 +381,7 @@ class FileListView extends HookConsumerWidget {
recycled.value = !recycled.value; recycled.value = !recycled.value;
unindexedNotifier.setRecycled(recycled.value); unindexedNotifier.setRecycled(recycled.value);
}, },
tooltip: tooltip: recycled.value
recycled.value
? 'Show Active Files' ? 'Show Active Files'
: 'Show Recycle Bin', : 'Show Recycle Bin',
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
@@ -429,11 +412,13 @@ class FileListView extends HookConsumerWidget {
if (mode.value == FileListMode.normal && currentPath.value == '/') if (mode.value == FileListMode.normal && currentPath.value == '/')
_buildUnindexedFilesEntry(ref).padding(bottom: 12), _buildUnindexedFilesEntry(ref).padding(bottom: 12),
Expanded( Expanded(
child: CustomScrollView( child:
CustomScrollView(
slivers: [bodyWidget, const SliverGap(12)], slivers: [bodyWidget, const SliverGap(12)],
).padding( ).padding(
horizontal: horizontal: viewMode.value == FileListViewMode.waterfall
viewMode.value == FileListViewMode.waterfall ? 12 : null, ? 12
: null,
), ),
), ),
if (isSelectionMode.value) if (isSelectionMode.value)
@@ -457,8 +442,7 @@ class FileListView extends HookConsumerWidget {
const Gap(12), const Gap(12),
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
final allIds = final allIds = currentVisibleItems.value
currentVisibleItems.value
.expand( .expand(
(item) => item.maybeMap( (item) => item.maybeMap(
file: (f) => [f.fileIndex.id], file: (f) => [f.fileIndex.id],
@@ -502,8 +486,7 @@ class FileListView extends HookConsumerWidget {
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Symbols.delete), icon: const Icon(Symbols.delete),
label: const Text('Delete'), label: const Text('Delete'),
onPressed: onPressed: selectedFileIds.value.isNotEmpty
selectedFileIds.value.isNotEmpty
? () async { ? () async {
final confirmed = await showConfirmAlert( final confirmed = await showConfirmAlert(
'Are you sure you want to delete the selected files?', 'Are you sure you want to delete the selected files?',
@@ -519,8 +502,8 @@ class FileListView extends HookConsumerWidget {
final resp = await client.post( final resp = await client.post(
'/drive/files/batches/delete', '/drive/files/batches/delete',
data: { data: {
'file_ids': 'file_ids': selectedFileIds.value
selectedFileIds.value.toList(), .toList(),
}, },
); );
final count = resp.data['count'] as int; final count = resp.data['count'] as int;
@@ -584,8 +567,7 @@ class FileListView extends HookConsumerWidget {
final item = items[index]; final item = items[index];
return item.map( return item.map(
file: file: (fileItem) => _buildWaterfallFileTile(
(fileItem) => _buildWaterfallFileTile(
fileItem, fileItem,
ref, ref,
context, context,
@@ -601,8 +583,7 @@ class FileListView extends HookConsumerWidget {
} }
}, },
), ),
folder: folder: (folderItem) =>
(folderItem) =>
_buildWaterfallFolderTile(folderItem, currentPath, context), _buildWaterfallFolderTile(folderItem, currentPath, context),
unindexedFile: (unindexedFileItem) { unindexedFile: (unindexedFileItem) {
// Should not happen // Should not happen
@@ -620,8 +601,7 @@ class FileListView extends HookConsumerWidget {
} }
final item = items[index]; final item = items[index];
return item.map( return item.map(
file: file: (fileItem) => _buildIndexedListTile(
(fileItem) => _buildIndexedListTile(
fileItem, fileItem,
ref, ref,
context, context,
@@ -637,8 +617,7 @@ class FileListView extends HookConsumerWidget {
} }
}, },
), ),
folder: folder: (folderItem) => ListTile(
(folderItem) => ListTile(
leading: ClipRRect( leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SizedBox( child: SizedBox(
@@ -654,8 +633,7 @@ class FileListView extends HookConsumerWidget {
), ),
subtitle: const Text('folder').tr(), subtitle: const Text('folder').tr(),
onTap: () { onTap: () {
final newPath = final newPath = currentPath.value == '/'
currentPath.value == '/'
? '/${folderItem.folderName}' ? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}'; : '${currentPath.value}/${folderItem.folderName}';
currentPath.value = newPath; currentPath.value = newPath;
@@ -705,8 +683,7 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath, ValueNotifier<String> currentPath,
) { ) {
return Card( return Card(
margin: margin: viewMode.value == FileListViewMode.waterfall
viewMode.value == FileListViewMode.waterfall
? const EdgeInsets.fromLTRB(0, 0, 0, 16) ? const EdgeInsets.fromLTRB(0, 0, 0, 16)
: const EdgeInsets.fromLTRB(12, 0, 12, 16), : const EdgeInsets.fromLTRB(12, 0, 12, 16),
child: Padding( child: Padding(
@@ -748,8 +725,8 @@ class FileListView extends HookConsumerWidget {
), ),
const Gap(12), const Gap(12),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: onPressed: () =>
() => onShowCreateDirectory(ref.context, currentPath), onShowCreateDirectory(ref.context, currentPath),
icon: const Icon(Symbols.create_new_folder), icon: const Icon(Symbols.create_new_folder),
label: const Text('Create Directory'), label: const Text('Create Directory'),
), ),
@@ -822,8 +799,9 @@ class FileListView extends HookConsumerWidget {
VoidCallback? toggleSelection, VoidCallback? toggleSelection,
) { ) {
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {}; final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
final ratio = final ratio = meta['ratio'] is num
meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0; ? (meta['ratio'] as num).toDouble()
: 1.0;
final itemType = file.mimeType?.split('/').first; final itemType = file.mimeType?.split('/').first;
final uri = final uri =
'${ref.read(apiClientProvider).options.baseUrl}/drive/files/${file.id}'; '${ref.read(apiClientProvider).options.baseUrl}/drive/files/${file.id}';
@@ -851,9 +829,7 @@ class FileListView extends HookConsumerWidget {
.read(apiClientProvider) .read(apiClientProvider)
.get(uri) .get(uri)
.then((response) => response.data as String), .then((response) => response.data as String),
builder: builder: (context, snapshot) => snapshot.hasData
(context, snapshot) =>
snapshot.hasData
? SingleChildScrollView( ? SingleChildScrollView(
padding: EdgeInsets.all(24), padding: EdgeInsets.all(24),
child: Text( child: Text(
@@ -961,8 +937,7 @@ class FileListView extends HookConsumerWidget {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: () { onTap: () {
final newPath = final newPath = currentPath.value == '/'
currentPath.value == '/'
? '/${folderItem.folderName}' ? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}'; : '${currentPath.value}/${folderItem.folderName}';
currentPath.value = newPath; currentPath.value = newPath;
@@ -1038,8 +1013,8 @@ class FileListView extends HookConsumerWidget {
// Should not happen in unindexed mode // Should not happen in unindexed mode
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
unindexedFile: unindexedFile: (unindexedFileItem) =>
(unindexedFileItem) => _buildWaterfallUnindexedFileTile( _buildWaterfallUnindexedFileTile(
unindexedFileItem, unindexedFileItem,
ref, ref,
context, context,
@@ -1077,17 +1052,14 @@ class FileListView extends HookConsumerWidget {
// Should not happen in unindexed mode // Should not happen in unindexed mode
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
unindexedFile: unindexedFile: (unindexedFileItem) => _buildUnindexedListTile(
(unindexedFileItem) => _buildUnindexedListTile(
unindexedFileItem, unindexedFileItem,
ref, ref,
context, context,
isSelectionMode.value, isSelectionMode.value,
selectedFileIds.value.contains(unindexedFileItem.file.id), selectedFileIds.value.contains(unindexedFileItem.file.id),
() { () {
if (selectedFileIds.value.contains( if (selectedFileIds.value.contains(unindexedFileItem.file.id)) {
unindexedFileItem.file.id,
)) {
selectedFileIds.value = Set.from(selectedFileIds.value) selectedFileIds.value = Set.from(selectedFileIds.value)
..remove(unindexedFileItem.file.id); ..remove(unindexedFileItem.file.id);
} else { } else {
@@ -1130,8 +1102,7 @@ class FileListView extends HookConsumerWidget {
), ),
], ],
), ),
title: title: file.name.isEmpty
file.name.isEmpty
? Text('untitled').tr().italic() ? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis), : Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(formatFileSize(file.size)), subtitle: Text(formatFileSize(file.size)),
@@ -1199,8 +1170,7 @@ class FileListView extends HookConsumerWidget {
), ),
], ],
), ),
title: title: file.name.isEmpty
file.name.isEmpty
? Text('untitled').tr().italic() ? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis), : Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(formatFileSize(file.size)), subtitle: Text(formatFileSize(file.size)),
@@ -1289,8 +1259,7 @@ class FileListView extends HookConsumerWidget {
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) { Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
return Card( return Card(
margin: margin: viewMode.value == FileListViewMode.waterfall
viewMode.value == FileListViewMode.waterfall
? EdgeInsets.zero ? EdgeInsets.zero
: const EdgeInsets.fromLTRB(12, 0, 12, 0), : const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: Padding( child: Padding(
@@ -1395,8 +1364,7 @@ class FileListView extends HookConsumerWidget {
ObjectRef<Timer?> queryDebounceTimer, ObjectRef<Timer?> queryDebounceTimer,
) { ) {
final poolDropdownItems = poolsAsync.when( final poolDropdownItems = poolsAsync.when(
data: data: (pools) => [
(pools) => [
const DropdownMenuItem<SnFilePool>( const DropdownMenuItem<SnFilePool>(
value: null, value: null,
child: Text('All Pools', style: TextStyle(fontSize: 14)), child: Text('All Pools', style: TextStyle(fontSize: 14)),
@@ -1416,8 +1384,7 @@ class FileListView extends HookConsumerWidget {
child: DropdownButton2<SnFilePool>( child: DropdownButton2<SnFilePool>(
value: selectedPool.value, value: selectedPool.value,
items: poolDropdownItems, items: poolDropdownItems,
onChanged: onChanged: isRefreshing
isRefreshing
? null ? null
: (value) { : (value) {
selectedPool.value = value; selectedPool.value = value;
@@ -1493,13 +1460,11 @@ class FileListView extends HookConsumerWidget {
final orderDropdown = DropdownButtonHideUnderline( final orderDropdown = DropdownButtonHideUnderline(
child: DropdownButton2<String>( child: DropdownButton2<String>(
value: order.value, value: order.value,
items: items: ['date', 'size', 'name']
['date', 'size', 'name']
.map( .map(
(e) => DropdownMenuItem( (e) => DropdownMenuItem(
value: e, value: e,
child: child: Text(
Text(
e == 'date' ? e : 'file${e.capitalizeEachWord()}', e == 'date' ? e : 'file${e.capitalizeEachWord()}',
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
).tr(), ).tr(),
@@ -1517,8 +1482,7 @@ class FileListView extends HookConsumerWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Center( child: Center(
child: child: Text(
Text(
(order.value ?? 'date') == 'date' (order.value ?? 'date') == 'date'
? (order.value ?? 'date') ? (order.value ?? 'date')
: 'file${order.value?.capitalizeEachWord()}', : 'file${order.value?.capitalizeEachWord()}',

View File

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/misc.dart'; import 'package:flutter_riverpod/misc.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/paging.dart'; import 'package:island/pods/paging.dart';
import 'package:island/widgets/extended_refresh_indicator.dart'; import 'package:island/widgets/extended_refresh_indicator.dart';
@@ -17,21 +18,27 @@ class PaginationList<T> extends HookConsumerWidget {
final ProviderListenable<AsyncValue<List<T>>> provider; final ProviderListenable<AsyncValue<List<T>>> provider;
final Refreshable<PaginationController<T>> notifier; final Refreshable<PaginationController<T>> notifier;
final Widget? Function(BuildContext, int, T) itemBuilder; final Widget? Function(BuildContext, int, T) itemBuilder;
final Widget? Function(BuildContext, int, T)? seperatorBuilder;
final double? spacing;
final bool isRefreshable; final bool isRefreshable;
final bool isSliver; final bool isSliver;
final bool showDefaultWidgets; final bool showDefaultWidgets;
final EdgeInsets? padding; final EdgeInsets? padding;
final Widget? footerSkeletonChild; final Widget? footerSkeletonChild;
final double? footerSkeletonMaxWidth;
const PaginationList({ const PaginationList({
super.key, super.key,
required this.provider, required this.provider,
required this.notifier, required this.notifier,
required this.itemBuilder, required this.itemBuilder,
this.seperatorBuilder,
this.spacing,
this.isRefreshable = true, this.isRefreshable = true,
this.isSliver = false, this.isSliver = false,
this.showDefaultWidgets = true, this.showDefaultWidgets = true,
this.padding, this.padding,
this.footerSkeletonChild, this.footerSkeletonChild,
this.footerSkeletonMaxWidth,
}); });
@override @override
@@ -39,9 +46,26 @@ class PaginationList<T> extends HookConsumerWidget {
final data = ref.watch(provider); final data = ref.watch(provider);
final noti = ref.watch(notifier); final noti = ref.watch(notifier);
if (data.isLoading && data.value?.isEmpty == true) { // For sliver cases, avoid animation to prevent complex sliver issues
final content = ResponseLoadingWidget(); if (isSliver) {
return isSliver ? SliverFillRemaining(child: content) : content; if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
),
);
return SliverList.list(children: content);
} }
if (data.hasError) { if (data.hasError) {
@@ -49,11 +73,10 @@ class PaginationList<T> extends HookConsumerWidget {
error: data.error, error: data.error,
onRetry: noti.refresh, onRetry: noti.refresh,
); );
return isSliver ? SliverFillRemaining(child: content) : content; return SliverFillRemaining(child: content);
} }
final listView = isSliver final listView = SuperSliverList.separated(
? SuperSliverList.builder(
itemCount: (data.value?.length ?? 0) + 1, itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
if (idx == data.value?.length) { if (idx == data.value?.length) {
@@ -61,14 +84,68 @@ class PaginationList<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
} }
final entry = data.value?[idx]; final entry = data.value?[idx];
if (entry != null) return itemBuilder(context, idx, entry); if (entry != null) return itemBuilder(context, idx, entry);
return null; return null;
}, },
) separatorBuilder: (context, index) {
: SuperListView.builder( if (seperatorBuilder != null) {
final entry = data.value?[index];
if (entry != null) {
return seperatorBuilder!(context, index, entry) ??
const SizedBox();
}
return const SizedBox();
}
if (spacing != null && spacing! > 0) {
return Gap(spacing!);
}
return const SizedBox();
},
);
return isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView;
}
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
),
);
return SizedBox(
key: const ValueKey('loading'),
child: ListView(children: content),
);
}
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SizedBox(key: const ValueKey('error'), child: content);
}
final listView = SuperListView.separated(
padding: padding, padding: padding,
itemCount: (data.value?.length ?? 0) + 1, itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
@@ -77,17 +154,41 @@ class PaginationList<T> extends HookConsumerWidget {
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
} }
final entry = data.value?[idx]; final entry = data.value?[idx];
if (entry != null) return itemBuilder(context, idx, entry); if (entry != null) return itemBuilder(context, idx, entry);
return null; return null;
}, },
separatorBuilder: (context, index) {
if (seperatorBuilder != null) {
final entry = data.value?[index];
if (entry != null) {
return seperatorBuilder!(context, index, entry) ??
const SizedBox();
}
return const SizedBox();
}
if (spacing != null && spacing! > 0) {
return Gap(spacing!);
}
return const SizedBox();
},
); );
return isRefreshable return SizedBox(
key: const ValueKey('data'),
child: isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView) ? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView; : listView,
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: buildContent(),
);
} }
} }
@@ -99,6 +200,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
final bool isSliver; final bool isSliver;
final bool showDefaultWidgets; final bool showDefaultWidgets;
final Widget? footerSkeletonChild; final Widget? footerSkeletonChild;
final double? footerSkeletonMaxWidth;
const PaginationWidget({ const PaginationWidget({
super.key, super.key,
required this.provider, required this.provider,
@@ -108,6 +210,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
this.isSliver = false, this.isSliver = false,
this.showDefaultWidgets = true, this.showDefaultWidgets = true,
this.footerSkeletonChild, this.footerSkeletonChild,
this.footerSkeletonMaxWidth,
}); });
@override @override
@@ -115,9 +218,26 @@ class PaginationWidget<T> extends HookConsumerWidget {
final data = ref.watch(provider); final data = ref.watch(provider);
final noti = ref.watch(notifier); final noti = ref.watch(notifier);
if (data.isLoading && data.value?.isEmpty == true) { // For sliver cases, avoid animation to prevent complex sliver issues
final content = ResponseLoadingWidget(); if (isSliver) {
return isSliver ? SliverFillRemaining(child: content) : content; if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
),
);
return SliverList.list(children: content);
} }
if (data.hasError) { if (data.hasError) {
@@ -125,13 +245,14 @@ class PaginationWidget<T> extends HookConsumerWidget {
error: data.error, error: data.error,
onRetry: noti.refresh, onRetry: noti.refresh,
); );
return isSliver ? SliverFillRemaining(child: content) : content; return SliverFillRemaining(child: content);
} }
final footer = PaginationListFooter( final footer = PaginationListFooter(
noti: noti, noti: noti,
data: data, data: data,
skeletonChild: footerSkeletonChild, skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
); );
final content = contentBuilder(data.value ?? [], footer); final content = contentBuilder(data.value ?? [], footer);
@@ -139,12 +260,68 @@ class PaginationWidget<T> extends HookConsumerWidget {
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content) ? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content; : content;
} }
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child:
footerSkeletonChild ??
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
),
);
return SizedBox(
key: const ValueKey('loading'),
child: ListView(children: content),
);
}
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SizedBox(key: const ValueKey('error'), child: content);
}
final footer = PaginationListFooter(
noti: noti,
data: data,
skeletonChild: footerSkeletonChild,
skeletonMaxWidth: footerSkeletonMaxWidth,
);
final content = contentBuilder(data.value ?? [], footer);
return SizedBox(
key: const ValueKey('data'),
child: isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content,
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: buildContent(),
);
}
} }
class PaginationListFooter<T> extends HookConsumerWidget { class PaginationListFooter<T> extends HookConsumerWidget {
final PaginationController<T> noti; final PaginationController<T> noti;
final AsyncValue<List<T>> data; final AsyncValue<List<T>> data;
final Widget? skeletonChild; final Widget? skeletonChild;
final double? skeletonMaxWidth;
final bool isSliver; final bool isSliver;
const PaginationListFooter({ const PaginationListFooter({
@@ -152,6 +329,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
required this.noti, required this.noti,
required this.data, required this.data,
this.skeletonChild, this.skeletonChild,
this.skeletonMaxWidth,
this.isSliver = false, this.isSliver = false,
}); });
@@ -161,13 +339,12 @@ class PaginationListFooter<T> extends HookConsumerWidget {
final placeholder = Skeletonizer( final placeholder = Skeletonizer(
enabled: true, enabled: true,
child: effect: ShimmerEffect(
skeletonChild ?? baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
ListTile( highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
title: Text('Some data'),
subtitle: const Text('Subtitle here'),
trailing: const Icon(Icons.ac_unit),
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: skeletonChild ?? _DefaultSkeletonChild(maxWidth: skeletonMaxWidth),
); );
final child = hasBeenVisible.value final child = hasBeenVisible.value
? data.isLoading ? data.isLoading
@@ -194,3 +371,26 @@ class PaginationListFooter<T> extends HookConsumerWidget {
); );
} }
} }
class _DefaultSkeletonChild extends StatelessWidget {
final double? maxWidth;
const _DefaultSkeletonChild({this.maxWidth});
@override
Widget build(BuildContext context) {
final content = ListTile(
title: Text('Some data'),
subtitle: const Text('Subtitle here'),
trailing: const Icon(Icons.ac_unit),
);
if (maxWidth != null) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth!),
child: content,
),
);
}
return content;
}
}

View File

@@ -9,33 +9,14 @@ import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart'; import 'package:island/models/post_tag.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/post/post_categories.dart';
import 'package:island/screens/realm/realms.dart'; import 'package:island/screens/realm/realms.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/compose_shared.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
part 'compose_settings_sheet.g.dart';
@riverpod
Future<List<SnPostCategory>> postCategories(Ref ref) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/sphere/posts/categories');
final categories =
resp.data
.map((e) => SnPostCategory.fromJson(e))
.cast<SnPostCategory>()
.toList();
// Remove duplicates based on id
final uniqueCategories = <String, SnPostCategory>{};
for (final category in categories) {
uniqueCategories[category.id] = category;
}
return uniqueCategories.values.toList();
}
class ComposeSettingsSheet extends HookConsumerWidget { class ComposeSettingsSheet extends HookConsumerWidget {
final ComposeState state; final ComposeState state;
@@ -121,8 +102,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
void showVisibilitySheet() { void showVisibilitySheet() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: builder: (context) => SheetScaffold(
(context) => SheetScaffold(
titleText: 'postVisibility'.tr(), titleText: 'postVisibility'.tr(),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -182,8 +162,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
// Tags field // Tags field
@@ -209,8 +189,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: children: currentTags.map((tag) {
currentTags.map((tag) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@@ -226,8 +205,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
Text( Text(
'#$tag', '#$tag',
style: TextStyle( style: TextStyle(
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.onPrimary, ).colorScheme.onPrimary,
fontSize: 14, fontSize: 14,
@@ -244,8 +222,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
child: Icon( child: Icon(
Icons.close, Icons.close,
size: 16, size: 16,
color: color: Theme.of(
Theme.of(
context, context,
).colorScheme.onPrimary, ).colorScheme.onPrimary,
), ),
@@ -274,8 +251,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
}, },
); );
}, },
suggestionsCallback: suggestionsCallback: (pattern) =>
(pattern) => _fetchTagSuggestions(pattern, ref), _fetchTagSuggestions(pattern, ref),
itemBuilder: (context, suggestion) { itemBuilder: (context, suggestion) {
return ListTile( return ListTile(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@@ -314,21 +291,17 @@ class ComposeSettingsSheet extends HookConsumerWidget {
), ),
), ),
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)), hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
items: items: (postCategories.value ?? <SnPostCategory>[]).map((item) {
(postCategories.value ?? <SnPostCategory>[]).map((item) {
return DropdownMenuItem( return DropdownMenuItem(
value: item, value: item,
enabled: false, enabled: false,
child: StatefulBuilder( child: StatefulBuilder(
builder: (context, menuSetState) { builder: (context, menuSetState) {
final isSelected = state.categories.value.contains( final isSelected = state.categories.value.contains(item);
item,
);
return InkWell( return InkWell(
onTap: () { onTap: () {
isSelected isSelected
? state.categories.value = ? state.categories.value = state.categories.value
state.categories.value
.where((e) => e != item) .where((e) => e != item)
.toList() .toList()
: state.categories.value = [ : state.categories.value = [
@@ -339,9 +312,7 @@ class ComposeSettingsSheet extends HookConsumerWidget {
}, },
child: Container( child: Container(
height: double.infinity, height: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16.0),
horizontal: 16.0,
),
child: Row( child: Row(
children: [ children: [
if (isSelected) if (isSelected)

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_settings_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(postCategories)
const postCategoriesProvider = PostCategoriesProvider._();
final class PostCategoriesProvider
extends
$FunctionalProvider<
AsyncValue<List<SnPostCategory>>,
List<SnPostCategory>,
FutureOr<List<SnPostCategory>>
>
with
$FutureModifier<List<SnPostCategory>>,
$FutureProvider<List<SnPostCategory>> {
const PostCategoriesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'postCategoriesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$postCategoriesHash();
@$internal
@override
$FutureProviderElement<List<SnPostCategory>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SnPostCategory>> create(Ref ref) {
return postCategories(ref);
}
}
String _$postCategoriesHash() => r'8799c10eb91cf8c8c7ea72eff3475e1eaa7b9a2b';

View File

@@ -24,7 +24,7 @@ import 'package:island/widgets/post/compose_link_attachments.dart';
import 'package:island/widgets/post/compose_poll.dart'; import 'package:island/widgets/post/compose_poll.dart';
import 'package:island/widgets/post/compose_fund.dart'; import 'package:island/widgets/post/compose_fund.dart';
import 'package:island/widgets/post/compose_recorder.dart'; import 'package:island/widgets/post/compose_recorder.dart';
import 'package:island/pods/file_pool.dart'; import 'package:island/pods/drive/file_pool.dart';
import 'package:pasteboard/pasteboard.dart'; import 'package:pasteboard/pasteboard.dart';
import 'package:island/talker.dart'; import 'package:island/talker.dart';
@@ -108,8 +108,8 @@ class ComposeLogic {
String? pollId; String? pollId;
String? fundId; String? fundId;
if (originalPost?.meta?['embeds'] is List) { if (originalPost?.meta?['embeds'] is List) {
final embeds = final embeds = (originalPost!.meta!['embeds'] as List)
(originalPost!.meta!['embeds'] as List).cast<Map<String, dynamic>>(); .cast<Map<String, dynamic>>();
try { try {
final pollEmbed = embeds.firstWhere((e) => e['type'] == 'poll'); final pollEmbed = embeds.firstWhere((e) => e['type'] == 'poll');
pollId = pollEmbed['id']; pollId = pollEmbed['id'];
@@ -202,8 +202,7 @@ class ComposeLogic {
final attachment = state.attachments.value[i]; final attachment = state.attachments.value[i];
if (attachment.data is! SnCloudFile) { if (attachment.data is! SnCloudFile) {
try { try {
final cloudFile = final cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile(
ref: ref, ref: ref,
fileData: attachment, fileData: attachment,
).future; ).future;
@@ -242,8 +241,7 @@ class ComposeLogic {
repliedPost: null, repliedPost: null,
forwardedPostId: null, forwardedPostId: null,
forwardedPost: null, forwardedPost: null,
attachments: attachments: state.attachments.value
state.attachments.value
.map((e) => e.data) .map((e) => e.data)
.whereType<SnCloudFile>() .whereType<SnCloudFile>()
.toList(), .toList(),
@@ -315,8 +313,7 @@ class ComposeLogic {
repliedPost: null, repliedPost: null,
forwardedPostId: null, forwardedPostId: null,
forwardedPost: null, forwardedPost: null,
attachments: attachments: state.attachments.value
state.attachments.value
.map((e) => e.data) .map((e) => e.data)
.whereType<SnCloudFile>() .whereType<SnCloudFile>()
.toList(), .toList(),
@@ -501,8 +498,7 @@ class ComposeLogic {
UniversalFile value, UniversalFile value,
int index, int index,
) { ) {
state.attachments.value = state.attachments.value = state.attachments.value.mapIndexed((idx, ele) {
state.attachments.value.mapIndexed((idx, ele) {
if (idx == index) return value; if (idx == index) return value;
return ele; return ele;
}).toList(); }).toList();
@@ -528,13 +524,11 @@ class ComposeLogic {
final pools = await ref.read(poolsProvider.future); final pools = await ref.read(poolsProvider.future);
final selectedPoolId = resolveDefaultPoolId(ref, pools); final selectedPoolId = resolveDefaultPoolId(ref, pools);
cloudFile = cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile(
ref: ref, ref: ref,
fileData: attachment, fileData: attachment,
poolId: poolId ?? selectedPoolId, poolId: poolId ?? selectedPoolId,
mode: mode: attachment.type == UniversalFileType.file
attachment.type == UniversalFileType.file
? FileUploadMode.generic ? FileUploadMode.generic
: FileUploadMode.mediaSafe, : FileUploadMode.mediaSafe,
onProgress: (progress, _) { onProgress: (progress, _) {
@@ -713,8 +707,7 @@ class ComposeLogic {
if (state.slugController.text.isNotEmpty) if (state.slugController.text.isNotEmpty)
'slug': state.slugController.text, 'slug': state.slugController.text,
'visibility': state.visibility.value, 'visibility': state.visibility.value,
'attachments': 'attachments': state.attachments.value
state.attachments.value
.where((e) => e.isOnCloud) .where((e) => e.isOnCloud)
.map((e) => e.data.id) .map((e) => e.data.id)
.toList(), .toList(),

View File

@@ -1,12 +1,17 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/screens/posts/compose.dart'; import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/post_detail.dart'; import 'package:island/screens/posts/post_detail.dart';
import 'package:island/services/compose_storage_db.dart'; import 'package:island/services/compose_storage_db.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_card.dart'; import 'package:island/widgets/post/compose_card.dart';
import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/compose_shared.dart';
@@ -32,12 +37,17 @@ class PostComposeSheet extends HookConsumerWidget {
SnPost? originalPost, SnPost? originalPost,
PostComposeInitialState? initialState, PostComposeInitialState? initialState,
}) { }) {
// Check if editing an article
if (originalPost != null && originalPost.type == 1) {
context.pushNamed('articleEdit', pathParameters: {'id': originalPost.id});
return Future.value(true);
}
return showModalBottomSheet<bool>( return showModalBottomSheet<bool>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true, useRootNavigator: true,
builder: builder: (context) => PostComposeSheet(
(context) => PostComposeSheet(
originalPost: originalPost, originalPost: originalPost,
initialState: initialState, initialState: initialState,
isBottomSheet: true, isBottomSheet: true,
@@ -52,8 +62,7 @@ class PostComposeSheet extends HookConsumerWidget {
final prompted = useState(false); final prompted = useState(false);
// Fetch full post data if we're editing a post // Fetch full post data if we're editing a post
final fullPostData = final fullPostData = originalPost != null
originalPost != null
? ref.watch(postProvider(originalPost!.id)) ? ref.watch(postProvider(originalPost!.id))
: const AsyncValue.data(null); : const AsyncValue.data(null);
@@ -115,7 +124,11 @@ class PostComposeSheet extends HookConsumerWidget {
}, [drafts, prompted.value]); }, [drafts, prompted.value]);
// Dispose state when widget is disposed // Dispose state when widget is disposed
useEffect(() => () => ComposeLogic.dispose(state), []); useEffect(
() =>
() => ComposeLogic.dispose(state),
[],
);
// Helper methods for actions // Helper methods for actions
void showSettingsSheet() { void showSettingsSheet() {
@@ -147,8 +160,7 @@ class PostComposeSheet extends HookConsumerWidget {
(state.submitting.value || state.currentPublisher.value == null) (state.submitting.value || state.currentPublisher.value == null)
? null ? null
: performSubmit, : performSubmit,
icon: icon: state.submitting.value
state.submitting.value
? SizedBox( ? SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@@ -157,14 +169,20 @@ class PostComposeSheet extends HookConsumerWidget {
: Icon( : Icon(
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload, effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
), ),
tooltip: tooltip: effectiveOriginalPost != null
effectiveOriginalPost != null
? 'postUpdate'.tr() ? 'postUpdate'.tr()
: 'postPublish'.tr(), : 'postPublish'.tr(),
), ),
]; ];
// Tablet will show a virtual keyboard, so we adjust the height factor accordingly
final isTablet =
isWideScreen(context) &&
!kIsWeb &&
(Platform.isAndroid || Platform.isAndroid);
return SheetScaffold( return SheetScaffold(
heightFactor: isTablet ? 0.95 : 0.8,
titleText: 'postCompose'.tr(), titleText: 'postCompose'.tr(),
actions: actions, actions: actions,
child: PostComposeCard( child: PostComposeCard(
@@ -192,8 +210,7 @@ class PostComposeSheet extends HookConsumerWidget {
final restore = await showDialog<bool>( final restore = await showDialog<bool>(
context: ref.context, context: ref.context,
useRootNavigator: true, useRootNavigator: true,
builder: builder: (context) => AlertDialog(
(context) => AlertDialog(
title: Text('restoreDraftTitle'.tr()), title: Text('restoreDraftTitle'.tr()),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -226,8 +243,7 @@ class PostComposeSheet extends HookConsumerWidget {
description: latestDraft.description, description: latestDraft.description,
content: latestDraft.content, content: latestDraft.content,
visibility: latestDraft.visibility, visibility: latestDraft.visibility,
attachments: attachments: latestDraft.attachments
latestDraft.attachments
.map((e) => UniversalFile.fromAttachment(e)) .map((e) => UniversalFile.fromAttachment(e))
.toList(), .toList(),
); );

View File

@@ -26,7 +26,12 @@ class PostItemSkeleton extends StatelessWidget {
final renderingPadding = final renderingPadding =
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
return Column( return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: EdgeInsets.only(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -41,13 +46,18 @@ class PostItemSkeleton extends StatelessWidget {
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
), ),
if (isShowReference) if (isShowReference)
_ReferencedPostWidgetSkeleton(renderingPadding: renderingPadding), _ReferencedPostWidgetSkeleton(
renderingPadding: renderingPadding,
),
if (isEmbedReply) if (isEmbedReply)
_PostReplyPreviewSkeleton( _PostReplyPreviewSkeleton(
renderingPadding: renderingPadding, renderingPadding: renderingPadding,
).padding(horizontal: renderingPadding.horizontal, top: 8), ).padding(horizontal: renderingPadding.horizontal, top: 8),
Gap(renderingPadding.vertical), Gap(renderingPadding.vertical),
], ],
),
),
),
); );
} }
} }

View File

@@ -1,81 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/post/post_list.dart';
import 'package:island/pods/paging.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_item_creator.dart'; import 'package:island/widgets/post/post_item_creator.dart';
import 'package:island/widgets/post/post_item_skeleton.dart'; import 'package:island/widgets/post/post_item_skeleton.dart';
part 'post_list.freezed.dart';
@freezed
sealed class PostListQuery with _$PostListQuery {
const factory PostListQuery({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
@Default(false) bool shuffle,
bool? includeReplies,
bool? mediaOnly,
String? queryTerm,
String? order,
int? periodStart,
int? periodEnd,
@Default(true) bool orderDesc,
}) = _PostListQuery;
}
final postListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<PostListNotifier, List<SnPost>, PostListQuery>(
PostListNotifier.new,
);
class PostListNotifier extends AsyncNotifier<List<SnPost>>
with AsyncPaginationController<SnPost> {
final PostListQuery arg;
PostListNotifier(this.arg);
static const int pageSize = 20;
@override
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': arg.includeReplies,
'orderDesc': arg.orderDesc,
if (arg.shuffle) 'shuffle': arg.shuffle,
if (arg.pubName != null) 'pub': arg.pubName,
if (arg.realm != null) 'realm': arg.realm,
if (arg.type != null) 'type': arg.type,
if (arg.tags != null) 'tags': arg.tags,
if (arg.categories != null) 'categories': arg.categories,
if (arg.pinned != null) 'pinned': arg.pinned,
if (arg.order != null) 'order': arg.order,
if (arg.periodStart != null) 'periodStart': arg.periodStart,
if (arg.periodEnd != null) 'periodEnd': arg.periodEnd,
if (arg.queryTerm != null) 'query': arg.queryTerm,
if (arg.mediaOnly != null) 'media': arg.mediaOnly,
};
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
return data.map((json) => SnPost.fromJson(json)).toList();
}
}
/// Defines which post item widget to use in the list /// Defines which post item widget to use in the list
enum PostItemType { enum PostItemType {
/// Regular post item with user information /// Regular post item with user information
@@ -86,21 +18,7 @@ enum PostItemType {
} }
class SliverPostList extends HookConsumerWidget { class SliverPostList extends HookConsumerWidget {
final String? pubName; final PostListQuery? query;
final String? realm;
final int? type;
final List<String>? categories;
final List<String>? tags;
final bool shuffle;
final bool? pinned;
final bool? includeReplies;
final bool? mediaOnly;
final String? queryTerm;
// Can be "populaurity", other value will be treated as "date"
final String? order;
final int? periodStart;
final int? periodEnd;
final bool? orderDesc;
final PostItemType itemType; final PostItemType itemType;
final Color? backgroundColor; final Color? backgroundColor;
final EdgeInsets? padding; final EdgeInsets? padding;
@@ -108,23 +26,11 @@ class SliverPostList extends HookConsumerWidget {
final Function? onRefresh; final Function? onRefresh;
final Function(SnPost)? onUpdate; final Function(SnPost)? onUpdate;
final double? maxWidth; final double? maxWidth;
final String? queryKey;
const SliverPostList({ const SliverPostList({
super.key, super.key,
this.pubName, this.query,
this.realm,
this.type,
this.categories,
this.tags,
this.shuffle = false,
this.pinned,
this.includeReplies,
this.mediaOnly,
this.queryTerm,
this.order,
this.orderDesc = true,
this.periodStart,
this.periodEnd,
this.itemType = PostItemType.regular, this.itemType = PostItemType.regular,
this.backgroundColor, this.backgroundColor,
this.padding, this.padding,
@@ -132,35 +38,37 @@ class SliverPostList extends HookConsumerWidget {
this.onRefresh, this.onRefresh,
this.onUpdate, this.onUpdate,
this.maxWidth, this.maxWidth,
this.queryKey,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final params = PostListQuery( final provider = postListProvider(
pubName: pubName, PostListQueryConfig(
realm: realm, id: queryKey,
type: type, initialFilter: query ?? PostListQuery(),
categories: categories, ),
tags: tags,
shuffle: shuffle,
pinned: pinned,
includeReplies: includeReplies,
mediaOnly: mediaOnly,
queryTerm: queryTerm,
order: order,
periodStart: periodStart,
periodEnd: periodEnd,
orderDesc: orderDesc ?? true,
); );
final provider = postListNotifierProvider(params); final notifier = ref.watch(provider.notifier);
final notifier = provider.notifier;
final currentFilter = useState(query ?? PostListQuery());
useEffect(() {
if (currentFilter.value != query) {
notifier.applyFilter(query ?? PostListQuery());
}
return null;
}, [query, queryKey]);
return PaginationList( return PaginationList(
provider: provider, provider: provider,
notifier: notifier, notifier: provider.notifier,
isRefreshable: false, isRefreshable: false,
isSliver: true, isSliver: true,
footerSkeletonChild: const PostItemSkeleton(), footerSkeletonChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: const PostItemSkeleton(),
),
itemBuilder: (context, index, post) { itemBuilder: (context, index, post) {
if (maxWidth != null) { if (maxWidth != null) {
return Center( return Center(

View File

@@ -113,10 +113,7 @@ return $default(_that);case _:
final _that = this; final _that = this;
switch (_that) { switch (_that) {
case _ReactionListQuery(): case _ReactionListQuery():
return $default(_that);case _: return $default(_that);}
throw StateError('Unexpected subclass');
}
} }
/// A variant of `map` that fallback to returning `null`. /// A variant of `map` that fallback to returning `null`.
/// ///
@@ -175,10 +172,7 @@ return $default(_that.symbol,_that.postId);case _:
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String symbol, String postId) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String symbol, String postId) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _ReactionListQuery(): case _ReactionListQuery():
return $default(_that.symbol,_that.postId);case _: return $default(_that.symbol,_that.postId);}
throw StateError('Unexpected subclass');
}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///

View File

@@ -5,6 +5,7 @@ import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart'; import 'package:island/pods/paging.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_item_skeleton.dart';
final postRepliesProvider = AsyncNotifierProvider.autoDispose.family( final postRepliesProvider = AsyncNotifierProvider.autoDispose.family(
PostRepliesNotifier.new, PostRepliesNotifier.new,
@@ -47,11 +48,24 @@ class PostRepliesList extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final provider = postRepliesProvider(postId); final provider = postRepliesProvider(postId);
final skeletonItem = Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: const PostItemSkeleton(),
);
return PaginationList( return PaginationList(
provider: provider, provider: provider,
notifier: provider.notifier, notifier: provider.notifier,
isRefreshable: false, isRefreshable: false,
isSliver: true, isSliver: true,
footerSkeletonChild: maxWidth == null
? skeletonItem
: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth!),
child: skeletonItem,
),
),
itemBuilder: (context, index, item) { itemBuilder: (context, index, item) {
final contentWidget = Card( final contentWidget = Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),

View File

@@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -66,6 +68,7 @@ class PostReplyPreview extends HookConsumerWidget {
final bool isOpenable; final bool isOpenable;
final bool isCompact; final bool isCompact;
final bool isAutoload; final bool isAutoload;
final double? itemMaxWidth;
final VoidCallback? onOpen; final VoidCallback? onOpen;
const PostReplyPreview({ const PostReplyPreview({
super.key, super.key,
@@ -73,6 +76,7 @@ class PostReplyPreview extends HookConsumerWidget {
this.isOpenable = false, this.isOpenable = false,
this.isCompact = false, this.isCompact = false,
this.isAutoload = true, this.isAutoload = true,
this.itemMaxWidth,
this.onOpen, this.onOpen,
}); });
@@ -114,17 +118,22 @@ class PostReplyPreview extends HookConsumerWidget {
return null; return null;
}, [parent]); }, [parent]);
final featuredReply = final featuredReply = isOpenable
isOpenable ? null : ref.watch(postFeaturedReplyProvider(parent.id)); ? null
: ref.watch(postFeaturedReplyProvider(parent.id));
final itemWidget = Widget itemBuilder(double maxWidth) {
isOpenable return isOpenable
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
for (final post in posts.value) for (final post in posts.value)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InkWell( InkWell(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8, spacing: 8,
@@ -142,12 +151,17 @@ class PostReplyPreview extends HookConsumerWidget {
) )
else else
Expanded( Expanded(
child: Text( child:
Text(
'postHasAttachments', 'postHasAttachments',
).plural(post.attachments.length), style: TextStyle(height: 2),
)
.plural(post.attachments.length)
.padding(top: 2),
), ),
], ],
), ),
),
onTap: () { onTap: () {
onOpen?.call(); onOpen?.call();
context.pushNamed( context.pushNamed(
@@ -162,12 +176,14 @@ class PostReplyPreview extends HookConsumerWidget {
isOpenable: true, isOpenable: true,
isCompact: true, isCompact: true,
isAutoload: false, isAutoload: false,
itemMaxWidth: math.max(maxWidth - 24, 200),
onOpen: onOpen, onOpen: onOpen,
).padding(left: 24), ).padding(left: 24),
], ],
), ),
if (loading.value) if (loading.value)
Row( Row(
mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
SizedBox( SizedBox(
@@ -179,8 +195,9 @@ class PostReplyPreview extends HookConsumerWidget {
], ],
) )
else if (posts.value.length < parent.repliesCount) else if (posts.value.length < parent.repliesCount)
InkWell( GestureDetector(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
const Icon(Symbols.keyboard_arrow_down, size: 20), const Icon(Symbols.keyboard_arrow_down, size: 20),
@@ -194,8 +211,9 @@ class PostReplyPreview extends HookConsumerWidget {
], ],
) )
: (featuredReply!).map( : (featuredReply!).map(
data: data: (data) => ConstrainedBox(
(data) => Row( constraints: BoxConstraints(maxWidth: maxWidth),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8, spacing: 8,
children: [ children: [
@@ -218,16 +236,15 @@ class PostReplyPreview extends HookConsumerWidget {
), ),
], ],
), ),
error: ),
(e) => Row( error: (e) => Row(
spacing: 8, spacing: 8,
children: [ children: [
const Icon(Symbols.close, size: 18), const Icon(Symbols.close, size: 18),
Text(e.error.toString()), Text(e.error.toString()),
], ],
), ),
loading: loading: (_) => Row(
(_) => Row(
spacing: 8, spacing: 8,
children: [ children: [
SizedBox( SizedBox(
@@ -239,10 +256,10 @@ class PostReplyPreview extends HookConsumerWidget {
], ],
), ),
); );
}
final contentWidget = final contentWidget = isCompact
isCompact ? itemBuilder(itemMaxWidth ?? MediaQuery.of(context).size.width)
? itemWidget
: Container( : Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -252,8 +269,11 @@ class PostReplyPreview extends HookConsumerWidget {
), ),
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: Column( width: double.infinity,
crossAxisAlignment: CrossAxisAlignment.stretch, child: LayoutBuilder(
builder: (context, constraints) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4, spacing: 4,
children: [ children: [
Text('repliesCount') Text('repliesCount')
@@ -261,13 +281,17 @@ class PostReplyPreview extends HookConsumerWidget {
.fontSize(15) .fontSize(15)
.bold() .bold()
.padding(horizontal: 5), .padding(horizontal: 5),
itemWidget, SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: itemBuilder(constraints.maxWidth),
),
], ],
);
},
), ),
); );
return InkWell( return GestureDetector(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -479,8 +503,9 @@ class ReferencedPostWidget extends StatelessWidget {
referencePost.description!, referencePost.description!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: color: Theme.of(
Theme.of(context).colorScheme.onSurfaceVariant, context,
).colorScheme.onSurfaceVariant,
), ),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -490,8 +515,7 @@ class ReferencedPostWidget extends StatelessWidget {
content: referencePost.content!, content: referencePost.content!,
textStyle: const TextStyle(fontSize: 14), textStyle: const TextStyle(fontSize: 14),
isSelectable: false, isSelectable: false,
linesMargin: linesMargin: referencePost.type == 0
referencePost.type == 0
? const EdgeInsets.only(bottom: 4) ? const EdgeInsets.only(bottom: 4)
: null, : null,
attachments: item.attachments, attachments: item.attachments,
@@ -537,8 +561,7 @@ class ReferencedPostWidget extends StatelessWidget {
} }
return content.gestures( return content.gestures(
onTap: onTap: () => context.pushNamed(
() => context.pushNamed(
'postDetail', 'postDetail',
pathParameters: {'id': referencePost!.id}, pathParameters: {'id': referencePost!.id},
), ),
@@ -577,8 +600,7 @@ class PostHeader extends StatelessWidget {
spacing: 12, spacing: 12,
children: [ children: [
GestureDetector( GestureDetector(
onTap: onTap: isInteractive
isInteractive
? () { ? () {
context.pushNamed( context.pushNamed(
'publisherProfile', 'publisherProfile',
@@ -627,8 +649,7 @@ class PostHeader extends StatelessWidget {
), ),
if (item.realm == null) if (item.realm == null)
Flexible( Flexible(
child: child: isCompact
isCompact
? const SizedBox.shrink() ? const SizedBox.shrink()
: Text( : Text(
'@${item.publisher.name}', '@${item.publisher.name}',
@@ -734,8 +755,7 @@ class PostBody extends ConsumerWidget {
const Icon(Symbols.label, size: 16).padding(top: 2), const Icon(Symbols.label, size: 16).padding(top: 2),
for (final tag in isFullPost ? item.tags : item.tags.take(3)) for (final tag in isFullPost ? item.tags : item.tags.take(3))
InkWell( InkWell(
onTap: onTap: isInteractive
isInteractive
? () { ? () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postTagDetail', 'postTagDetail',
@@ -761,8 +781,7 @@ class PostBody extends ConsumerWidget {
for (final category for (final category
in isFullPost ? item.categories : item.categories.take(2)) in isFullPost ? item.categories : item.categories.take(2))
InkWell( InkWell(
onTap: onTap: isInteractive
isInteractive
? () { ? () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postCategoryDetail', 'postCategoryDetail',
@@ -798,8 +817,7 @@ class PostBody extends ConsumerWidget {
hideOverlay hideOverlay
? text ? text
: Tooltip( : Tooltip(
message: message: !isFullPost && isRelativeTime
!isFullPost && isRelativeTime
? item.editedAt!.formatSystem() ? item.editedAt!.formatSystem()
: item.editedAt!.formatRelative(context), : item.editedAt!.formatRelative(context),
child: text, child: text,
@@ -936,8 +954,7 @@ class PostBody extends ConsumerWidget {
], ],
).padding(bottom: 4), ).padding(bottom: 4),
MarkdownTextContent( MarkdownTextContent(
content: content: item.isTruncated
item.isTruncated
? '${item.content!}...' ? '${item.content!}...'
: item.content ?? '', : item.content ?? '',
isSelectable: isTextSelectable, isSelectable: isTextSelectable,

View File

@@ -3,22 +3,26 @@ import 'package:flutter/material.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart'; import 'package:flutter_card_swiper/flutter_card_swiper.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
const kShufflePostListId = 'shuffle';
class PostShuffleScreen extends HookConsumerWidget { class PostShuffleScreen extends HookConsumerWidget {
const PostShuffleScreen({super.key}); const PostShuffleScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
const params = PostListQuery(shuffle: true); const query = PostListQuery(shuffle: true);
final postListState = ref.watch(postListNotifierProvider(params)); final cfg = PostListQueryConfig(
final postListNotifier = ref.watch( id: kShufflePostListId,
postListNotifierProvider(params).notifier, initialFilter: query,
); );
final postListState = ref.watch(postListProvider(cfg));
final postListNotifier = ref.watch(postListProvider(cfg).notifier);
final cardSwiperController = useMemoized(() => CardSwiperController(), []); final cardSwiperController = useMemoized(() => CardSwiperController(), []);
@@ -46,7 +50,8 @@ class PostShuffleScreen extends HookConsumerWidget {
controller: cardSwiperController, controller: cardSwiperController,
cardsCount: items.length, cardsCount: items.length,
isLoop: false, isLoop: false,
cardBuilder: ( cardBuilder:
(
context, context,
index, index,
horizontalOffsetPercentage, horizontalOffsetPercentage,
@@ -62,7 +67,9 @@ class PostShuffleScreen extends HookConsumerWidget {
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
child: PostActionableItem(item: items[index]), child: PostActionableItem(
item: items[index],
),
), ),
), ),
), ),
@@ -91,8 +98,7 @@ class PostShuffleScreen extends HookConsumerWidget {
bottom: MediaQuery.of(context).padding.bottom, bottom: MediaQuery.of(context).padding.bottom,
), ),
height: kBottomControlHeight, height: kBottomControlHeight,
child: child: Row(
Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(

View File

@@ -0,0 +1,348 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart';
class PostFilterWidget extends StatefulWidget {
final TabController categoryTabController;
final PostListQuery initialQuery;
final ValueChanged<PostListQuery> onQueryChanged;
final bool hideSearch;
const PostFilterWidget({
super.key,
required this.categoryTabController,
required this.initialQuery,
required this.onQueryChanged,
this.hideSearch = false,
});
@override
State<PostFilterWidget> createState() => _PostFilterWidgetState();
}
class _PostFilterWidgetState extends State<PostFilterWidget> {
late bool? _includeReplies;
late bool _mediaOnly;
late String? _queryTerm;
late String? _order;
late bool _orderDesc;
late int? _periodStart;
late int? _periodEnd;
late int? _type;
late bool _showAdvancedFilters;
late TextEditingController _searchController;
@override
void initState() {
super.initState();
_includeReplies = widget.initialQuery.includeReplies;
_mediaOnly = widget.initialQuery.mediaOnly ?? false;
_queryTerm = widget.initialQuery.queryTerm;
_order = widget.initialQuery.order;
_orderDesc = widget.initialQuery.orderDesc;
_periodStart = widget.initialQuery.periodStart;
_periodEnd = widget.initialQuery.periodEnd;
_type = widget.initialQuery.type;
_showAdvancedFilters = false;
_searchController = TextEditingController(text: _queryTerm);
widget.categoryTabController.addListener(_onTabChanged);
}
@override
void dispose() {
widget.categoryTabController.removeListener(_onTabChanged);
_searchController.dispose();
super.dispose();
}
void _onTabChanged() {
final tabIndex = widget.categoryTabController.index;
setState(() {
_type = switch (tabIndex) {
1 => 0,
2 => 1,
_ => null,
};
});
_updateQuery();
}
void _updateQuery() {
final newQuery = widget.initialQuery.copyWith(
includeReplies: _includeReplies,
mediaOnly: _mediaOnly,
queryTerm: _queryTerm,
order: _order,
periodStart: _periodStart,
periodEnd: _periodEnd,
orderDesc: _orderDesc,
type: _type,
);
widget.onQueryChanged(newQuery);
}
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
children: [
TabBar(
controller: widget.categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
const Divider(height: 1),
Column(
children: [
Row(
children: [
Expanded(
child: CheckboxListTile(
title: Text('reply'.tr()),
value: _includeReplies,
tristate: true,
onChanged: (value) {
// Cycle through: null -> false -> true -> null
setState(() {
if (_includeReplies == null) {
_includeReplies = false;
} else if (_includeReplies == false) {
_includeReplies = true;
} else {
_includeReplies = null;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.reply),
),
),
Expanded(
child: CheckboxListTile(
title: Text('attachments'.tr()),
value: _mediaOnly,
onChanged: (value) {
setState(() {
if (value != null) {
_mediaOnly = value;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.attachment),
),
),
],
),
CheckboxListTile(
title: Text('descendingOrder'.tr()),
value: _orderDesc,
onChanged: (value) {
setState(() {
if (value != null) {
_orderDesc = value;
}
});
_updateQuery();
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.sort),
),
],
),
const Divider(height: 1),
ListTile(
title: Text('advancedFilters'.tr()),
leading: const Icon(Symbols.filter_list),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
trailing: Icon(
_showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more,
),
onTap: () {
setState(() {
_showAdvancedFilters = !_showAdvancedFilters;
});
},
),
if (_showAdvancedFilters) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!widget.hideSearch)
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'search'.tr(),
hintText: 'searchPosts'.tr(),
prefixIcon: const Icon(Symbols.search),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
onChanged: (value) {
setState(() {
_queryTerm = value.isEmpty ? null : value;
});
_updateQuery();
},
),
if (!widget.hideSearch) const Gap(12),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'sortBy'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
value: _order,
items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem(
value: 'popularity',
child: Text('popularity'.tr()),
),
],
onChanged: (value) {
setState(() {
_order = value;
});
_updateQuery();
},
),
const Gap(12),
Row(
children: [
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _periodStart != null
? DateTime.fromMillisecondsSinceEpoch(
_periodStart! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
setState(() {
_periodStart =
pickedDate.millisecondsSinceEpoch ~/ 1000;
});
_updateQuery();
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'fromDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
_periodStart != null
? DateTime.fromMillisecondsSinceEpoch(
_periodStart! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
const Gap(8),
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: _periodEnd != null
? DateTime.fromMillisecondsSinceEpoch(
_periodEnd! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
setState(() {
_periodEnd =
pickedDate.millisecondsSinceEpoch ~/ 1000;
});
_updateQuery();
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'toDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
_periodEnd != null
? DateTime.fromMillisecondsSinceEpoch(
_periodEnd! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
],
),
],
),
),
],
],
),
);
}
}

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart'; import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart'; import 'package:island/pods/paging.dart';
import 'package:island/widgets/paging/pagination_list.dart'; import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/realm/realm_card.dart'; import 'package:island/widgets/realm/realm_list_tile.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
final realmListNotifierProvider = AsyncNotifierProvider.autoDispose final realmListNotifierProvider = AsyncNotifierProvider.autoDispose
@@ -51,25 +50,12 @@ class SliverRealmList extends HookConsumerWidget {
notifier: provider.notifier, notifier: provider.notifier,
isSliver: true, isSliver: true,
isRefreshable: false, isRefreshable: false,
spacing: 8,
itemBuilder: (context, index, realm) { itemBuilder: (context, index, realm) {
return Column( return ConstrainedBox(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child:
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 540), constraints: const BoxConstraints(maxWidth: 540),
child: RealmCard(realm: realm), child: RealmListTile(realm: realm),
).center(), ).center();
),
if (index <
(ref.read(provider).value?.length ?? 0) -
1) // Add gap except for last item? Actually PaginationList handles loading indicator which might look like last item.
// Wait, ref.read(provider).value?.length might change.
// Simpler to just add bottom padding to all, or Gap.
const Gap(8),
],
);
}, },
); );
} }

Some files were not shown because too many files have changed in this diff Show More