Compare commits
25 Commits
9d03faf594
...
3.5.0+151
| Author | SHA1 | Date | |
|---|---|---|---|
|
d7746d14e4
|
|||
|
648d5225f6
|
|||
|
9d4d0f2e48
|
|||
|
fe386163f4
|
|||
|
ac2cee10e5
|
|||
|
9c370647dd
|
|||
|
7516e197fe
|
|||
|
71c372ab6c
|
|||
|
25f23f7f93
|
|||
|
51853698b9
|
|||
|
39ed5393ab
|
|||
|
782b3f1b08
|
|||
|
3ef2f13dd3
|
|||
|
36b0f55a47
|
|||
|
bc7a6e865e
|
|||
|
2ff60fc4ff
|
|||
|
ea93aa144e
|
|||
|
e4cd0c99df
|
|||
|
dff84dde58
|
|||
|
16c7b7e764
|
|||
|
240509ceff
|
|||
|
91da9768c1
|
|||
|
60b8e2bcad
|
|||
|
504e4d55ad
|
|||
|
38a15bb62a
|
@@ -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" />
|
||||||
@@ -159,4 +161,4 @@
|
|||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1489,5 +1489,6 @@
|
|||||||
"accountActivationAlert": "请记住激活您的账户",
|
"accountActivationAlert": "请记住激活您的账户",
|
||||||
"accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。",
|
"accountActivationAlertHint": "未激活的账户可能会导致各种权限问题,请点击我们发送到您邮箱收件箱的链接来激活您的账户。",
|
||||||
"accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。",
|
"accountActivationResendHint": "没收到?请尝试点击下方按钮重新发送。如果您在账户未激活期间需要更新邮箱,请随时联系我们的客服。",
|
||||||
"accountActivationResend": "重新发送"
|
"accountActivationResend": "重新发送",
|
||||||
|
"noFurtherData": "已经到底了"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,18 +12,16 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
|
Map<String, dynamic> _$SnAbuseReportToJson(_SnAbuseReport instance) =>
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ _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>,
|
),
|
||||||
),
|
|
||||||
badges:
|
badges:
|
||||||
(json['badges'] as List<dynamic>?)
|
(json['badges'] as List<dynamic>?)
|
||||||
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnAccountBadge.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -31,16 +30,14 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
|
||||||
@@ -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,69 +83,55 @@ 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,
|
||||||
id: json['id'] as String,
|
) => _SnAccountProfile(
|
||||||
firstName: json['first_name'] as String? ?? '',
|
id: json['id'] as String,
|
||||||
middleName: json['middle_name'] as String? ?? '',
|
firstName: json['first_name'] as String? ?? '',
|
||||||
lastName: json['last_name'] as String? ?? '',
|
middleName: json['middle_name'] as String? ?? '',
|
||||||
bio: json['bio'] as String? ?? '',
|
lastName: json['last_name'] as String? ?? '',
|
||||||
gender: json['gender'] as String? ?? '',
|
bio: json['bio'] as String? ?? '',
|
||||||
pronouns: json['pronouns'] as String? ?? '',
|
gender: json['gender'] as String? ?? '',
|
||||||
location: json['location'] as String? ?? '',
|
pronouns: json['pronouns'] as String? ?? '',
|
||||||
timeZone: json['time_zone'] as String? ?? '',
|
location: json['location'] as String? ?? '',
|
||||||
birthday:
|
timeZone: json['time_zone'] as String? ?? '',
|
||||||
json['birthday'] == null
|
birthday: 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: json['last_seen_at'] == null
|
||||||
lastSeenAt:
|
? null
|
||||||
json['last_seen_at'] == null
|
: DateTime.parse(json['last_seen_at'] as String),
|
||||||
? null
|
activeBadge: json['active_badge'] == null
|
||||||
: DateTime.parse(json['last_seen_at'] as String),
|
? null
|
||||||
activeBadge:
|
: SnAccountBadge.fromJson(json['active_badge'] as Map<String, dynamic>),
|
||||||
json['active_badge'] == null
|
experience: (json['experience'] as num).toInt(),
|
||||||
? null
|
level: (json['level'] as num).toInt(),
|
||||||
: SnAccountBadge.fromJson(
|
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
|
||||||
json['active_badge'] as Map<String, dynamic>,
|
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
|
||||||
),
|
levelingProgress: (json['leveling_progress'] as num).toDouble(),
|
||||||
experience: (json['experience'] as num).toInt(),
|
picture: json['picture'] == null
|
||||||
level: (json['level'] as num).toInt(),
|
? null
|
||||||
socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100,
|
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
|
||||||
socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0,
|
background: json['background'] == null
|
||||||
levelingProgress: (json['leveling_progress'] as num).toDouble(),
|
? null
|
||||||
picture:
|
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||||
json['picture'] == null
|
verification: json['verification'] == null
|
||||||
? null
|
? null
|
||||||
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
|
: SnVerificationMark.fromJson(
|
||||||
background:
|
json['verification'] as Map<String, dynamic>,
|
||||||
json['background'] == null
|
),
|
||||||
? null
|
usernameColor: json['username_color'] == null
|
||||||
: SnCloudFile.fromJson(
|
? null
|
||||||
json['background'] as Map<String, dynamic>,
|
: UsernameColor.fromJson(json['username_color'] as Map<String, dynamic>),
|
||||||
),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
verification:
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
json['verification'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: SnVerificationMark.fromJson(
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['verification'] as Map<String, dynamic>,
|
);
|
||||||
),
|
|
||||||
usernameColor:
|
|
||||||
json['username_color'] == null
|
|
||||||
? null
|
|
||||||
: UsernameColor.fromJson(
|
|
||||||
json['username_color'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
|
||||||
deletedAt:
|
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
@@ -188,17 +172,15 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
|
Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
|
||||||
@@ -225,21 +207,18 @@ _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: json['deleted_at'] == null
|
||||||
deletedAt:
|
? null
|
||||||
json['deleted_at'] == null
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
|
Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
|
||||||
@@ -261,20 +240,18 @@ _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,
|
||||||
isPublic: json['is_public'] as bool,
|
isPublic: json['is_public'] as bool,
|
||||||
content: json['content'] as String,
|
content: json['content'] 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnContactMethodToJson(_SnContactMethod instance) =>
|
Map<String, dynamic> _$SnContactMethodToJson(_SnContactMethod instance) =>
|
||||||
@@ -295,10 +272,9 @@ _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,
|
||||||
topic: json['topic'] as String,
|
topic: json['topic'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
@@ -306,10 +282,9 @@ _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,10 +351,9 @@ _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,10 +379,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnExperienceRecordToJson(_SnExperienceRecord instance) =>
|
Map<String, dynamic> _$SnExperienceRecordToJson(_SnExperienceRecord instance) =>
|
||||||
@@ -430,16 +403,14 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnSocialCreditRecordToJson(
|
Map<String, dynamic> _$SnSocialCreditRecordToJson(
|
||||||
@@ -460,10 +431,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnFriendOverviewItemToJson(
|
Map<String, dynamic> _$SnFriendOverviewItemToJson(
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnNotableDayToJson(_SnNotableDay instance) =>
|
Map<String, dynamic> _$SnNotableDayToJson(_SnNotableDay instance) =>
|
||||||
@@ -35,10 +34,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnTimelineEventToJson(_SnTimelineEvent instance) =>
|
Map<String, dynamic> _$SnTimelineEventToJson(_SnTimelineEvent instance) =>
|
||||||
@@ -56,21 +54,18 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnCheckInResultToJson(_SnCheckInResult instance) =>
|
Map<String, dynamic> _$SnCheckInResultToJson(_SnCheckInResult instance) =>
|
||||||
@@ -103,16 +98,14 @@ _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: (json['statuses'] as List<dynamic>)
|
||||||
statuses:
|
.map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>))
|
||||||
(json['statuses'] as List<dynamic>)
|
.toList(),
|
||||||
.map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnEventCalendarEntryToJson(
|
Map<String, dynamic> _$SnEventCalendarEntryToJson(
|
||||||
@@ -141,10 +134,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPresenceActivityToJson(_SnPresenceActivity instance) =>
|
Map<String, dynamic> _$SnPresenceActivityToJson(_SnPresenceActivity instance) =>
|
||||||
|
|||||||
@@ -34,35 +34,29 @@ 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>,
|
||||||
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,
|
||||||
nonce: json['nonce'] as String?,
|
nonce: json['nonce'] as String?,
|
||||||
location:
|
location: json['location'] == null
|
||||||
json['location'] == null
|
? null
|
||||||
? null
|
: GeoIpLocation.fromJson(json['location'] as Map<String, dynamic>),
|
||||||
: GeoIpLocation.fromJson(
|
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
|
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
|
||||||
@@ -90,28 +84,23 @@ _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(json['location'] as Map<String, dynamic>),
|
||||||
: GeoIpLocation.fromJson(
|
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
|
Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
|
||||||
@@ -138,18 +127,15 @@ _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: json['expired_at'] == null
|
||||||
expiredAt:
|
? null
|
||||||
json['expired_at'] == null
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
? null
|
enabledAt: json['enabled_at'] == null
|
||||||
: DateTime.parse(json['expired_at'] as String),
|
? null
|
||||||
enabledAt:
|
: DateTime.parse(json['enabled_at'] as String),
|
||||||
json['enabled_at'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['enabled_at'] as String),
|
|
||||||
trustworthy: (json['trustworthy'] as num).toInt(),
|
trustworthy: (json['trustworthy'] as num).toInt(),
|
||||||
createdResponse: json['created_response'] as Map<String, dynamic>?,
|
createdResponse: json['created_response'] as Map<String, dynamic>?,
|
||||||
);
|
);
|
||||||
@@ -177,10 +163,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnAccountConnectionToJson(
|
Map<String, dynamic> _$SnAccountConnectionToJson(
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ 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,10 +28,9 @@ 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?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ _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>),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
|
Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{
|
||||||
@@ -74,10 +73,9 @@ _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? ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,30 +13,25 @@ _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: json['background'] == null
|
||||||
background:
|
? null
|
||||||
json['background'] == null
|
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: 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: (json['members'] as List<dynamic>?)
|
||||||
members:
|
?.map((e) => SnChatMember.fromJson(e as Map<String, dynamic>))
|
||||||
(json['members'] as List<dynamic>?)
|
.toList(),
|
||||||
?.map((e) => SnChatMember.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
|
Map<String, dynamic> _$SnChatRoomToJson(_SnChatRoom instance) =>
|
||||||
@@ -62,10 +57,9 @@ _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,
|
||||||
type: json['type'] as String? ?? 'text',
|
type: json['type'] as String? ?? 'text',
|
||||||
content: json['content'] as String?,
|
content: json['content'] as String?,
|
||||||
@@ -76,10 +70,9 @@ _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:
|
||||||
(json['attachments'] as List<dynamic>?)
|
(json['attachments'] as List<dynamic>?)
|
||||||
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -122,10 +115,9 @@ _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,
|
||||||
messageId: json['message_id'] as String,
|
messageId: json['message_id'] as String,
|
||||||
senderId: json['sender_id'] as String,
|
senderId: json['sender_id'] as String,
|
||||||
@@ -151,42 +143,33 @@ _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: json['break_until'] == null
|
||||||
breakUntil:
|
? null
|
||||||
json['break_until'] == null
|
: DateTime.parse(json['break_until'] as String),
|
||||||
? null
|
timeoutUntil: json['timeout_until'] == null
|
||||||
: DateTime.parse(json['break_until'] as String),
|
? null
|
||||||
timeoutUntil:
|
: DateTime.parse(json['timeout_until'] as String),
|
||||||
json['timeout_until'] == null
|
status: json['status'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['timeout_until'] as String),
|
: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
|
||||||
status:
|
lastTyped: json['last_typed'] == null
|
||||||
json['status'] == null
|
? null
|
||||||
? null
|
: DateTime.parse(json['last_typed'] as String),
|
||||||
: SnAccountStatus.fromJson(
|
|
||||||
json['status'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
lastTyped:
|
|
||||||
json['last_typed'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['last_typed'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
|
Map<String, dynamic> _$SnChatMemberToJson(_SnChatMember instance) =>
|
||||||
@@ -211,12 +194,11 @@ 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>,
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
|
Map<String, dynamic> _$SnChatSummaryToJson(_SnChatSummary instance) =>
|
||||||
@@ -251,10 +233,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
|
Map<String, dynamic> _$ChatRealtimeJoinResponseToJson(
|
||||||
@@ -288,14 +269,12 @@ _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: json['ended_at'] == null
|
||||||
endedAt:
|
? null
|
||||||
json['ended_at'] == null
|
: DateTime.parse(json['ended_at'] as String),
|
||||||
? null
|
|
||||||
: DateTime.parse(json['ended_at'] as String),
|
|
||||||
senderId: json['sender_id'] as String,
|
senderId: json['sender_id'] as String,
|
||||||
sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>),
|
sender: SnChatMember.fromJson(json['sender'] as Map<String, dynamic>),
|
||||||
roomId: json['room_id'] as String,
|
roomId: json['room_id'] as String,
|
||||||
|
|||||||
@@ -12,30 +12,25 @@ _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: json['background'] == null
|
||||||
background:
|
? null
|
||||||
json['background'] == null
|
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||||
? null
|
verification: json['verification'] == null
|
||||||
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
? null
|
||||||
verification:
|
: SnVerificationMark.fromJson(
|
||||||
json['verification'] == null
|
json['verification'] as Map<String, dynamic>,
|
||||||
? null
|
),
|
||||||
: SnVerificationMark.fromJson(
|
oauthConfig: json['oauth_config'] == null
|
||||||
json['verification'] as Map<String, dynamic>,
|
? null
|
||||||
),
|
: CustomAppOauthConfig.fromJson(
|
||||||
oauthConfig:
|
json['oauth_config'] as Map<String, dynamic>,
|
||||||
json['oauth_config'] == null
|
),
|
||||||
? null
|
links: json['links'] == null
|
||||||
: CustomAppOauthConfig.fromJson(
|
? null
|
||||||
json['oauth_config'] as Map<String, dynamic>,
|
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
|
||||||
),
|
|
||||||
links:
|
|
||||||
json['links'] == null
|
|
||||||
? null
|
|
||||||
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
|
|
||||||
secrets:
|
secrets:
|
||||||
(json['secrets'] as List<dynamic>?)
|
(json['secrets'] as List<dynamic>?)
|
||||||
?.map((e) => CustomAppSecret.fromJson(e as Map<String, dynamic>))
|
?.map((e) => CustomAppSecret.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -83,10 +78,9 @@ _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:
|
||||||
(json['allowed_scopes'] as List<dynamic>?)
|
(json['allowed_scopes'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
@@ -118,10 +112,9 @@ _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,
|
||||||
appId: json['app_id'] as String? ?? '',
|
appId: json['app_id'] as String? ?? '',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ 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>),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
|
Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) =>
|
||||||
|
|||||||
@@ -22,10 +22,9 @@ _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?,
|
||||||
bundleId: json['bundle_id'] as String?,
|
bundleId: json['bundle_id'] as String?,
|
||||||
encryptPassword: json['encrypt_password'] as String?,
|
encryptPassword: json['encrypt_password'] as String?,
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnScrappedLinkToJson(_SnScrappedLink instance) =>
|
Map<String, dynamic> _$SnScrappedLinkToJson(_SnScrappedLink instance) =>
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ _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:
|
||||||
(json['sensitive_marks'] as List<dynamic>?)
|
(json['sensitive_marks'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => (e as num).toInt())
|
||||||
@@ -47,17 +46,15 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
|
||||||
@@ -87,10 +84,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnCloudFileIndexToJson(_SnCloudFileIndex instance) =>
|
Map<String, dynamic> _$SnCloudFileIndexToJson(_SnCloudFileIndex instance) =>
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ _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: json['updated_at'] == null
|
||||||
updatedAt:
|
? null
|
||||||
json['updated_at'] == null
|
: DateTime.parse(json['updated_at'] as String),
|
||||||
? null
|
deletedAt: json['deleted_at'] == null
|
||||||
: DateTime.parse(json['updated_at'] as String),
|
? null
|
||||||
deletedAt:
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) =>
|
Map<String, dynamic> _$SnFilePoolToJson(_SnFilePool instance) =>
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnHeatmapToJson(_SnHeatmap instance) =>
|
Map<String, dynamic> _$SnHeatmapToJson(_SnHeatmap instance) =>
|
||||||
|
|||||||
@@ -8,31 +8,25 @@ 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(json['user_answer'] as Map<String, dynamic>),
|
||||||
: SnPollAnswer.fromJson(
|
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
||||||
@@ -52,27 +46,23 @@ 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPollToJson(_SnPoll instance) => <String, dynamic>{
|
Map<String, dynamic> _$SnPollToJson(_SnPoll instance) => <String, dynamic>{
|
||||||
@@ -92,10 +82,9 @@ _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,
|
||||||
description: json['description'] as String?,
|
description: json['description'] as String?,
|
||||||
order: (json['order'] as num).toInt(),
|
order: (json['order'] as num).toInt(),
|
||||||
@@ -145,14 +134,12 @@ _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: json['account'] == null
|
||||||
account:
|
? null
|
||||||
json['account'] == null
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPollAnswerToJson(_SnPollAnswer instance) =>
|
Map<String, dynamic> _$SnPollAnswerToJson(_SnPollAnswer instance) =>
|
||||||
|
|||||||
@@ -11,25 +11,20 @@ _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: json['published_at'] == null
|
||||||
publishedAt:
|
? null
|
||||||
json['published_at'] == null
|
: DateTime.parse(json['published_at'] as String),
|
||||||
? null
|
|
||||||
: DateTime.parse(json['published_at'] as String),
|
|
||||||
visibility: (json['visibility'] as num?)?.toInt() ?? 0,
|
visibility: (json['visibility'] as num?)?.toInt() ?? 0,
|
||||||
content: json['content'] as String?,
|
content: json['content'] as String?,
|
||||||
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(json['embed_view'] as Map<String, dynamic>),
|
||||||
: SnPostEmbedView.fromJson(
|
|
||||||
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,25 +33,21 @@ _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:
|
||||||
(json['attachments'] as List<dynamic>?)
|
(json['attachments'] as List<dynamic>?)
|
||||||
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -90,18 +81,15 @@ _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: json['updated_at'] == null
|
||||||
updatedAt:
|
? null
|
||||||
json['updated_at'] == null
|
: DateTime.parse(json['updated_at'] as String),
|
||||||
? null
|
deletedAt: json['deleted_at'] == null
|
||||||
: DateTime.parse(json['updated_at'] as String),
|
? null
|
||||||
deletedAt:
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
repliedGone: json['replied_gone'] as bool? ?? false,
|
repliedGone: json['replied_gone'] as bool? ?? false,
|
||||||
forwardedGone: json['forwarded_gone'] as bool? ?? false,
|
forwardedGone: json['forwarded_gone'] as bool? ?? false,
|
||||||
isTruncated: json['is_truncated'] as bool? ?? false,
|
isTruncated: json['is_truncated'] as bool? ?? false,
|
||||||
@@ -214,18 +202,15 @@ _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: json['updated_at'] == null
|
||||||
updatedAt:
|
? null
|
||||||
json['updated_at'] == null
|
: DateTime.parse(json['updated_at'] as String),
|
||||||
? null
|
deletedAt: json['deleted_at'] == null
|
||||||
: DateTime.parse(json['updated_at'] as String),
|
? null
|
||||||
deletedAt:
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPostAwardToJson(_SnPostAward instance) =>
|
Map<String, dynamic> _$SnPostAwardToJson(_SnPostAward instance) =>
|
||||||
@@ -250,14 +235,12 @@ _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: json['deleted_at'] == null
|
||||||
deletedAt:
|
? null
|
||||||
json['deleted_at'] == null
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
? null
|
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) =>
|
Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) =>
|
||||||
@@ -278,17 +261,15 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPostFeaturedRecordToJson(
|
Map<String, dynamic> _$SnPostFeaturedRecordToJson(
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
|
Map<String, dynamic> _$SnPublicationSiteToJson(_SnPublicationSite instance) =>
|
||||||
|
|||||||
@@ -12,38 +12,31 @@ _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: json['background'] == null
|
||||||
background:
|
? null
|
||||||
json['background'] == null
|
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||||
? null
|
account: json['account'] == null
|
||||||
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
? null
|
||||||
account:
|
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||||
json['account'] == null
|
|
||||||
? null
|
|
||||||
: 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: json['updated_at'] == null
|
||||||
updatedAt:
|
? null
|
||||||
json['updated_at'] == null
|
: DateTime.parse(json['updated_at'] as String),
|
||||||
? null
|
deletedAt: json['deleted_at'] == null
|
||||||
: DateTime.parse(json['updated_at'] as String),
|
? null
|
||||||
deletedAt:
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: 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>,
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||||
@@ -67,26 +60,22 @@ 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnPublisherMemberToJson(_SnPublisherMember instance) =>
|
Map<String, dynamic> _$SnPublisherMemberToJson(_SnPublisherMember instance) =>
|
||||||
|
|||||||
@@ -12,27 +12,23 @@ _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: json['background'] == null
|
||||||
background:
|
? null
|
||||||
json['background'] == null
|
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
|
Map<String, dynamic> _$SnRealmToJson(_SnRealm instance) => <String, dynamic>{
|
||||||
@@ -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: json['status'] == null
|
||||||
status:
|
? null
|
||||||
json['status'] == null
|
: SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: SnAccountStatus.fromJson(
|
|
||||||
json['status'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) =>
|
Map<String, dynamic> _$SnRealmMemberToJson(_SnRealmMember instance) =>
|
||||||
|
|||||||
@@ -9,22 +9,19 @@ 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ReferenceToJson(_Reference instance) =>
|
Map<String, dynamic> _$ReferenceToJson(_Reference instance) =>
|
||||||
|
|||||||
@@ -8,26 +8,22 @@ 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: json['updated_at'] == null
|
||||||
updatedAt:
|
? null
|
||||||
json['updated_at'] == null
|
: DateTime.parse(json['updated_at'] as String),
|
||||||
? null
|
deletedAt: json['deleted_at'] == null
|
||||||
: DateTime.parse(json['updated_at'] as String),
|
? null
|
||||||
deletedAt:
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
json['deleted_at'] == null
|
|
||||||
? null
|
|
||||||
: 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(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,14 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) =>
|
Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) =>
|
||||||
@@ -42,20 +40,17 @@ _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: json['publisher'] == null
|
||||||
publisher:
|
? null
|
||||||
json['publisher'] == null
|
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: 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:
|
||||||
(json['stickers'] as List<dynamic>?)
|
(json['stickers'] as List<dynamic>?)
|
||||||
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
||||||
|
|||||||
@@ -16,14 +16,12 @@ _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: (json['attached_messages'] as List<dynamic>?)
|
||||||
attachedMessages:
|
?.map((e) => e as Map<String, dynamic>)
|
||||||
(json['attached_messages'] as List<dynamic>?)
|
.toList(),
|
||||||
?.map((e) => e as Map<String, dynamic>)
|
|
||||||
.toList(),
|
|
||||||
serviceId: json['service_id'] as String?,
|
serviceId: json['service_id'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,18 +85,14 @@ _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(json['function_call'] as Map<String, dynamic>),
|
||||||
: SnFunctionCall.fromJson(
|
functionResult: json['function_result'] == null
|
||||||
json['function_call'] as Map<String, dynamic>,
|
? null
|
||||||
),
|
: SnFunctionResult.fromJson(
|
||||||
functionResult:
|
json['function_result'] as Map<String, dynamic>,
|
||||||
json['function_result'] == null
|
),
|
||||||
? null
|
|
||||||
: SnFunctionResult.fromJson(
|
|
||||||
json['function_result'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnThinkingMessagePartToJson(
|
Map<String, dynamic> _$SnThinkingMessagePartToJson(
|
||||||
@@ -119,10 +113,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnThinkingSequenceToJson(_SnThinkingSequence instance) =>
|
Map<String, dynamic> _$SnThinkingSequenceToJson(_SnThinkingSequence instance) =>
|
||||||
@@ -159,18 +152,16 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
|
Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
|
||||||
@@ -206,10 +197,9 @@ _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(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ThoughtServicesResponseToJson(
|
Map<String, dynamic> _$ThoughtServicesResponseToJson(
|
||||||
|
|||||||
@@ -8,21 +8,18 @@ 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
||||||
@@ -77,10 +74,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletPocketToJson(_SnWalletPocket instance) =>
|
Map<String, dynamic> _$SnWalletPocketToJson(_SnWalletPocket instance) =>
|
||||||
@@ -102,21 +98,18 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
|
Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
|
||||||
@@ -140,10 +133,9 @@ _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,
|
||||||
isActive: json['is_active'] as bool? ?? true,
|
isActive: json['is_active'] as bool? ?? true,
|
||||||
isFreeTrial: json['is_free_trial'] as bool? ?? false,
|
isFreeTrial: json['is_free_trial'] as bool? ?? false,
|
||||||
@@ -153,23 +145,20 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletSubscriptionToJson(
|
Map<String, dynamic> _$SnWalletSubscriptionToJson(
|
||||||
@@ -204,10 +193,9 @@ _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),
|
||||||
identifier: json['identifier'] as String,
|
identifier: json['identifier'] as String,
|
||||||
);
|
);
|
||||||
@@ -239,10 +227,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
|
Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
|
||||||
@@ -269,43 +256,36 @@ _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: json['expired_at'] == null
|
||||||
expiredAt:
|
? null
|
||||||
json['expired_at'] == null
|
: DateTime.parse(json['expired_at'] as String),
|
||||||
? null
|
|
||||||
: 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletGiftToJson(_SnWalletGift instance) =>
|
Map<String, dynamic> _$SnWalletGiftToJson(_SnWalletGift instance) =>
|
||||||
@@ -330,35 +310,31 @@ 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(),
|
remainingAmount: (json['remaining_amount'] as num).toDouble(),
|
||||||
remainingAmount: (json['remaining_amount'] as num).toDouble(),
|
amountOfSplits: (json['amount_of_splits'] as num).toInt(),
|
||||||
amountOfSplits: (json['amount_of_splits'] as num).toInt(),
|
splitType: (json['split_type'] as num).toInt(),
|
||||||
splitType: (json['split_type'] as num).toInt(),
|
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: json['creator_account'] == null
|
||||||
creatorAccount:
|
|
||||||
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,24 +362,19 @@ _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(json['recipient_account'] as Map<String, dynamic>),
|
||||||
: SnAccount.fromJson(
|
|
||||||
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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWalletFundRecipientToJson(
|
Map<String, dynamic> _$SnWalletFundRecipientToJson(
|
||||||
@@ -425,33 +396,29 @@ _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: (json['region_one_numbers'] as List<dynamic>)
|
||||||
regionOneNumbers:
|
.map((e) => (e as num).toInt())
|
||||||
(json['region_one_numbers'] as List<dynamic>)
|
.toList(),
|
||||||
.map((e) => (e as num).toInt())
|
|
||||||
.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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnLotteryTicketToJson(_SnLotteryTicket instance) =>
|
Map<String, dynamic> _$SnLotteryTicketToJson(_SnLotteryTicket instance) =>
|
||||||
@@ -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,
|
||||||
id: json['id'] as String,
|
) => _SnLotteryRecord(
|
||||||
drawDate: DateTime.parse(json['draw_date'] as String),
|
id: json['id'] as String,
|
||||||
winningRegionOneNumbers:
|
drawDate: DateTime.parse(json['draw_date'] as String),
|
||||||
(json['winning_region_one_numbers'] as List<dynamic>)
|
winningRegionOneNumbers: (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: json['deleted_at'] == null
|
||||||
deletedAt:
|
? null
|
||||||
json['deleted_at'] == null
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
? null
|
);
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SnLotteryRecordToJson(_SnLotteryRecord instance) =>
|
Map<String, dynamic> _$SnLotteryRecordToJson(_SnLotteryRecord instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ _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: json['config'] == null
|
||||||
config:
|
? const SnWebFeedConfig()
|
||||||
json['config'] == null
|
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
|
||||||
? const SnWebFeedConfig()
|
|
||||||
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
|
|
||||||
publisherId: json['publisher_id'] as String,
|
publisherId: json['publisher_id'] as String,
|
||||||
articles:
|
articles:
|
||||||
(json['articles'] as List<dynamic>?)
|
(json['articles'] as List<dynamic>?)
|
||||||
@@ -33,10 +31,9 @@ _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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWebFeedToJson(_SnWebFeed instance) =>
|
Map<String, dynamic> _$SnWebFeedToJson(_SnWebFeed instance) =>
|
||||||
@@ -61,28 +58,22 @@ _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(json['preview'] as Map<String, dynamic>),
|
||||||
: SnScrappedLink.fromJson(
|
feed: json['feed'] == null
|
||||||
json['preview'] as Map<String, dynamic>,
|
? null
|
||||||
),
|
: SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>),
|
||||||
feed:
|
|
||||||
json['feed'] == null
|
|
||||||
? null
|
|
||||||
: 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),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SnWebArticleToJson(_SnWebArticle instance) =>
|
Map<String, dynamic> _$SnWebArticleToJson(_SnWebArticle instance) =>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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,12 +76,12 @@ 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();
|
||||||
|
|
||||||
final List<FileListItem> items = [
|
final List<FileListItem> items = [
|
||||||
...folders.map((folderName) => FileListItem.folder(folderName)),
|
...folders.map((folderName) => FileListItem.folder(folderName)),
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
lib/pods/post/post_categories.dart
Normal file
52
lib/pods/post/post_categories.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/pods/post/post_list.dart
Normal file
95
lib/pods/post/post_list.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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,10 +33,9 @@ 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();
|
|
||||||
|
|
||||||
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
|
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
final client = ref.watch(apiClientProvider);
|
||||||
) async {
|
final response = await client.get('/sphere/publishers/$pubName/feeds');
|
||||||
final client = ref.watch(apiClientProvider);
|
return (response.data as List)
|
||||||
final response = await client.get('/sphere/publishers/$pubName/feeds');
|
.map((json) => SnWebFeed.fromJson(json))
|
||||||
return (response.data as List)
|
.toList();
|
||||||
.map((json) => SnWebFeed.fromJson(json))
|
});
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
|
|
||||||
class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
||||||
final ({String pubName, String? feedId}) arg;
|
final ({String pubName, String? feedId}) arg;
|
||||||
@@ -51,10 +49,9 @@ 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());
|
|
||||||
|
|
||||||
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
|
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
|
|||||||
112
lib/route.dart
112
lib/route.dart
@@ -105,10 +105,9 @@ 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?,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'articleEdit',
|
name: 'articleEdit',
|
||||||
@@ -190,12 +189,11 @@ 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,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
@@ -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,12 +253,11 @@ 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,
|
),
|
||||||
),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'chatList',
|
name: 'chatList',
|
||||||
@@ -303,12 +295,11 @@ 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,
|
),
|
||||||
),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'realmNew',
|
name: 'realmNew',
|
||||||
@@ -336,12 +327,11 @@ 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,
|
),
|
||||||
),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'account',
|
name: 'account',
|
||||||
@@ -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,29 +506,25 @@ 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:
|
initialProjectId: state.uri.queryParameters['project'],
|
||||||
state.uri.queryParameters['publisher'],
|
),
|
||||||
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']!,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'developerProjectDetail',
|
name: 'developerProjectDetail',
|
||||||
@@ -558,22 +544,20 @@ 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']!,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
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']!,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,11 +45,10 @@ 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();
|
|
||||||
|
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
@@ -68,39 +67,36 @@ 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: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
credits < 100
|
||||||
children: [
|
? 'socialCreditsLevelPoor'.tr()
|
||||||
Text(
|
: credits < 150
|
||||||
credits < 100
|
? 'socialCreditsLevelNormal'.tr()
|
||||||
? 'socialCreditsLevelPoor'.tr()
|
: credits < 200
|
||||||
: credits < 150
|
? 'socialCreditsLevelGood'.tr()
|
||||||
? 'socialCreditsLevelNormal'.tr()
|
: 'socialCreditsLevelExcellent'.tr(),
|
||||||
: credits < 200
|
).tr().bold().fontSize(20),
|
||||||
? 'socialCreditsLevelGood'.tr()
|
Text('${credits.toStringAsFixed(2)} pts').fontSize(14),
|
||||||
: 'socialCreditsLevelExcellent'.tr(),
|
const Gap(8),
|
||||||
).tr().bold().fontSize(20),
|
LinearProgressIndicator(value: credits / 200),
|
||||||
Text(
|
|
||||||
'${credits.toStringAsFixed(2)} pts',
|
|
||||||
).fontSize(14),
|
|
||||||
const Gap(8),
|
|
||||||
LinearProgressIndicator(value: credits / 200),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Symbols.info),
|
|
||||||
tooltip: 'socialCreditsDescription'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
tooltip: 'socialCreditsDescription'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
error: (_, _) => Text('Error loading credits'),
|
error: (_, _) => Text('Error loading credits'),
|
||||||
loading: () => const LinearProgressIndicator(),
|
loading: () => const LinearProgressIndicator(),
|
||||||
)
|
)
|
||||||
@@ -119,15 +115,14 @@ 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(
|
context,
|
||||||
context,
|
).colorScheme.onSurface.withOpacity(0.8),
|
||||||
).colorScheme.onSurface.withOpacity(0.8),
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
|
|||||||
@@ -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,11 +35,10 @@ 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();
|
|
||||||
|
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
@@ -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,38 +186,35 @@ 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,
|
children: [
|
||||||
children: [
|
Text(record.reason),
|
||||||
Text(record.reason),
|
Row(
|
||||||
Row(
|
spacing: 4,
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
record.createdAt.formatRelative(context),
|
|
||||||
).fontSize(13),
|
|
||||||
Text('·').fontSize(13).bold(),
|
|
||||||
Text(record.createdAt.formatSystem()).fontSize(13),
|
|
||||||
],
|
|
||||||
).opacity(0.8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
|
record.createdAt.formatRelative(context),
|
||||||
),
|
).fontSize(13),
|
||||||
if (record.bonusMultiplier != 1.0)
|
Text('·').fontSize(13).bold(),
|
||||||
Text('x${record.bonusMultiplier}'),
|
Text(record.createdAt.formatSystem()).fontSize(13),
|
||||||
],
|
],
|
||||||
),
|
).opacity(0.8),
|
||||||
minTileHeight: 56,
|
],
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
),
|
||||||
),
|
subtitle: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Text('${record.delta > 0 ? '+' : ''}${record.delta} EXP'),
|
||||||
|
if (record.bonusMultiplier != 1.0)
|
||||||
|
Text('x${record.bonusMultiplier}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
minTileHeight: 56,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SliverGap(20),
|
SliverGap(20),
|
||||||
@@ -249,11 +246,10 @@ 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;
|
|
||||||
|
|
||||||
// Draw connecting lines between stairs
|
// Draw connecting lines between stairs
|
||||||
for (int i = 0; i < totalLevels - 1; i++) {
|
for (int i = 0; i < totalLevels - 1; i++) {
|
||||||
|
|||||||
@@ -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,11 +45,10 @@ 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();
|
|
||||||
|
|
||||||
totalCount = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
|
totalCount = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
|
||||||
|
|
||||||
@@ -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,64 +138,56 @@ 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: [
|
if (isPending && onAccept != null)
|
||||||
if (isPending && onAccept != null)
|
IconButton(
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
onPressed: submitting ? null : onAccept,
|
||||||
onPressed: submitting ? null : onAccept,
|
icon: const Icon(Symbols.check),
|
||||||
icon: const Icon(Symbols.check),
|
),
|
||||||
),
|
if (isPending && onDecline != null)
|
||||||
if (isPending && onDecline != null)
|
IconButton(
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
onPressed: submitting ? null : onDecline,
|
||||||
onPressed: submitting ? null : onDecline,
|
icon: const Icon(Symbols.close),
|
||||||
icon: const Icon(Symbols.close),
|
),
|
||||||
),
|
if (isWaiting && onCancel != null)
|
||||||
if (isWaiting && onCancel != null)
|
IconButton(
|
||||||
IconButton(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
onPressed: submitting ? null : onCancel,
|
||||||
onPressed: submitting ? null : onCancel,
|
icon: const Icon(Symbols.close),
|
||||||
icon: const Icon(Symbols.close),
|
),
|
||||||
),
|
if (isEstablished && onUpdateStatus != null)
|
||||||
if (isEstablished && onUpdateStatus != null)
|
PopupMenuButton(
|
||||||
PopupMenuButton(
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
icon: const Icon(Symbols.more_vert),
|
||||||
icon: const Icon(Symbols.more_vert),
|
itemBuilder: (context) => [
|
||||||
itemBuilder:
|
if (relationship.status >= 100) // If friend
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
if (relationship.status >= 100) // If friend
|
child: ListTile(
|
||||||
PopupMenuItem(
|
leading: const Icon(Symbols.block),
|
||||||
child: ListTile(
|
title: Text('blockUser').tr(),
|
||||||
leading: const Icon(Symbols.block),
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text('blockUser').tr(),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
onTap: () => onUpdateStatus?.call(relationship, -100),
|
||||||
),
|
)
|
||||||
onTap:
|
else if (relationship.status <= -100) // If blocked
|
||||||
() => onUpdateStatus?.call(
|
PopupMenuItem(
|
||||||
relationship,
|
child: ListTile(
|
||||||
-100,
|
leading: const Icon(Symbols.person_add),
|
||||||
),
|
title: Text('unblockUser').tr(),
|
||||||
)
|
contentPadding: EdgeInsets.zero,
|
||||||
else if (relationship.status <= -100) // If blocked
|
),
|
||||||
PopupMenuItem(
|
onTap: () => onUpdateStatus?.call(relationship, 100),
|
||||||
child: ListTile(
|
),
|
||||||
leading: const Icon(Symbols.person_add),
|
],
|
||||||
title: Text('unblockUser').tr(),
|
),
|
||||||
contentPadding: EdgeInsets.zero,
|
],
|
||||||
),
|
)
|
||||||
onTap:
|
: null,
|
||||||
() =>
|
|
||||||
onUpdateStatus?.call(relationship, 100),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,28 +373,26 @@ 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) =>
|
? Center(
|
||||||
items.isEmpty
|
child: Text(
|
||||||
? Center(
|
'friendSentRequestEmpty'.tr(),
|
||||||
child: Text(
|
textAlign: TextAlign.center,
|
||||||
'friendSentRequestEmpty'.tr(),
|
),
|
||||||
textAlign: TextAlign.center,
|
)
|
||||||
),
|
: ListView.builder(
|
||||||
)
|
shrinkWrap: true,
|
||||||
: ListView.builder(
|
itemCount: items.length,
|
||||||
shrinkWrap: true,
|
itemBuilder: (context, index) {
|
||||||
itemCount: items.length,
|
final request = items[index];
|
||||||
itemBuilder: (context, index) {
|
return RelationshipListTile(
|
||||||
final request = items[index];
|
relationship: request,
|
||||||
return RelationshipListTile(
|
onCancel: () => cancelRequest(request),
|
||||||
relationship: request,
|
currentUserId: user.value?.id,
|
||||||
onCancel: () => cancelRequest(request),
|
showRelatedAccount: true,
|
||||||
currentUserId: user.value?.id,
|
);
|
||||||
showRelatedAccount: true,
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,84 +51,124 @@ 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
? Text(
|
children: [
|
||||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
...previousChildren,
|
||||||
maxLines: 1,
|
if (currentChild != null) currentChild,
|
||||||
)
|
],
|
||||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
),
|
||||||
}
|
child: summary.when(
|
||||||
|
data: (data) => Container(
|
||||||
return Column(
|
key: const ValueKey('data'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
child: data == null
|
||||||
children: [
|
? isDirect && room.description == null
|
||||||
if (data.unreadCount > 0)
|
? Text(
|
||||||
Text(
|
validMembers
|
||||||
'unreadMessages'.plural(data.unreadCount),
|
.map((e) => '@${e.account.name}')
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
.join(', '),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
room.description ?? 'descriptionNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (data.unreadCount > 0)
|
||||||
|
Text(
|
||||||
|
'unreadMessages'.plural(data.unreadCount),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (data.lastMessage == null)
|
||||||
|
Text(
|
||||||
|
room.description ?? 'descriptionNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Badge(
|
||||||
|
label: Text(
|
||||||
|
data.lastMessage!.sender.account.nick,
|
||||||
|
),
|
||||||
|
textColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
(data.lastMessage!.content?.isNotEmpty ?? false)
|
||||||
|
? data.lastMessage!.content!
|
||||||
|
: 'messageNone'.tr(),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
RelativeTime(
|
||||||
|
context,
|
||||||
|
).format(data.lastMessage!.createdAt),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (data.lastMessage == null)
|
loading: () => Container(
|
||||||
Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1)
|
key: const ValueKey('loading'),
|
||||||
else
|
child: Builder(
|
||||||
Row(
|
builder: (context) {
|
||||||
spacing: 4,
|
final seed = DateTime.now().microsecondsSinceEpoch;
|
||||||
children: [
|
final len = 4 + (seed % 17); // 4..20 inclusive
|
||||||
Badge(
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
label: Text(data.lastMessage!.sender.account.nick),
|
var s = seed;
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
final buffer = StringBuffer();
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
for (var i = 0; i < len; i++) {
|
||||||
),
|
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
||||||
Expanded(
|
buffer.write(chars[s % chars.length]);
|
||||||
child: Text(
|
}
|
||||||
(data.lastMessage!.content?.isNotEmpty ?? false)
|
return Skeletonizer(
|
||||||
? data.lastMessage!.content!
|
enabled: true,
|
||||||
: 'messageNone'.tr(),
|
child: Text(buffer.toString()),
|
||||||
maxLines: 1,
|
);
|
||||||
overflow: TextOverflow.ellipsis,
|
},
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
),
|
||||||
),
|
),
|
||||||
),
|
error: (_, _) => Container(
|
||||||
Align(
|
key: const ValueKey('error'),
|
||||||
alignment: Alignment.centerRight,
|
child: isDirect && room.description == null
|
||||||
child: Text(
|
? Text(
|
||||||
RelativeTime(
|
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||||
context,
|
maxLines: 1,
|
||||||
).format(data.lastMessage!.createdAt),
|
)
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error:
|
|
||||||
(_, _) =>
|
|
||||||
isDirect && room.description == null
|
|
||||||
? Text(
|
|
||||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
|
||||||
maxLines: 1,
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
room.description ?? 'descriptionNone'.tr(),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,17 +190,15 @@ 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: validMembers
|
||||||
filesId:
|
.map((e) => e.account.profile.picture?.id)
|
||||||
validMembers
|
.toList(),
|
||||||
.map((e) => e.account.profile.picture?.id)
|
)
|
||||||
.toList(),
|
: room.picture?.id == null
|
||||||
)
|
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||||
: room.picture?.id == null
|
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
|
||||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
|
||||||
),
|
),
|
||||||
title: Text(titleText),
|
title: Text(titleText),
|
||||||
subtitle: buildSubtitle(),
|
subtitle: buildSubtitle(),
|
||||||
@@ -199,74 +238,67 @@ 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,
|
),
|
||||||
),
|
|
||||||
orElse: () => const SizedBox.shrink(),
|
orElse: () => const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: chats.when(
|
child: chats.when(
|
||||||
data:
|
data: (items) => RefreshIndicator(
|
||||||
(items) => RefreshIndicator(
|
onRefresh: () => Future.sync(() {
|
||||||
onRefresh:
|
ref.invalidate(chatRoomJoinedProvider);
|
||||||
() => Future.sync(() {
|
}),
|
||||||
ref.invalidate(chatRoomJoinedProvider);
|
child: SuperListView.builder(
|
||||||
}),
|
padding: EdgeInsets.only(bottom: 96),
|
||||||
child: SuperListView.builder(
|
itemCount: items
|
||||||
padding: EdgeInsets.only(bottom: 96),
|
.where(
|
||||||
itemCount:
|
(item) =>
|
||||||
items
|
selectedTab.value == 0 ||
|
||||||
.where(
|
(selectedTab.value == 1 && item.type == 1) ||
|
||||||
(item) =>
|
(selectedTab.value == 2 && item.type != 1),
|
||||||
selectedTab.value == 0 ||
|
)
|
||||||
(selectedTab.value == 1 && item.type == 1) ||
|
.length,
|
||||||
(selectedTab.value == 2 && item.type != 1),
|
itemBuilder: (context, index) {
|
||||||
)
|
final filteredItems = items
|
||||||
.length,
|
.where(
|
||||||
itemBuilder: (context, index) {
|
(item) =>
|
||||||
final filteredItems =
|
selectedTab.value == 0 ||
|
||||||
items
|
(selectedTab.value == 1 && item.type == 1) ||
|
||||||
.where(
|
(selectedTab.value == 2 && item.type != 1),
|
||||||
(item) =>
|
)
|
||||||
selectedTab.value == 0 ||
|
.toList();
|
||||||
(selectedTab.value == 1 &&
|
final item = filteredItems[index];
|
||||||
item.type == 1) ||
|
return ChatRoomListTile(
|
||||||
(selectedTab.value == 2 && item.type != 1),
|
room: item,
|
||||||
)
|
isDirect: item.type == 1,
|
||||||
.toList();
|
onTap: () {
|
||||||
final item = filteredItems[index];
|
if (isWideScreen(context)) {
|
||||||
return ChatRoomListTile(
|
context.replaceNamed(
|
||||||
room: item,
|
'chatRoom',
|
||||||
isDirect: item.type == 1,
|
pathParameters: {'id': item.id},
|
||||||
onTap: () {
|
);
|
||||||
if (isWideScreen(context)) {
|
} else {
|
||||||
context.replaceNamed(
|
context.pushNamed(
|
||||||
'chatRoom',
|
'chatRoom',
|
||||||
pathParameters: {'id': item.id},
|
pathParameters: {'id': item.id},
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
context.pushNamed(
|
|
||||||
'chatRoom',
|
|
||||||
pathParameters: {'id': item.id},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
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,53 +584,47 @@ class _ChatInvitesSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: invites.when(
|
child: invites.when(
|
||||||
data:
|
data: (items) => items.isEmpty
|
||||||
(items) =>
|
? Center(
|
||||||
items.isEmpty
|
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
|
||||||
? Center(
|
)
|
||||||
child:
|
: ListView.builder(
|
||||||
Text(
|
shrinkWrap: true,
|
||||||
'invitesEmpty',
|
itemCount: items.length,
|
||||||
textAlign: TextAlign.center,
|
itemBuilder: (context, index) {
|
||||||
).tr(),
|
final invite = items[index];
|
||||||
)
|
return ChatRoomListTile(
|
||||||
: ListView.builder(
|
room: invite.chatRoom!,
|
||||||
shrinkWrap: true,
|
isDirect: invite.chatRoom!.type == 1,
|
||||||
itemCount: items.length,
|
subtitle: Row(
|
||||||
itemBuilder: (context, index) {
|
spacing: 6,
|
||||||
final invite = items[index];
|
children: [
|
||||||
return ChatRoomListTile(
|
if (invite.chatRoom!.type == 1)
|
||||||
room: invite.chatRoom!,
|
Badge(
|
||||||
isDirect: invite.chatRoom!.type == 1,
|
label: const Text('directMessage').tr(),
|
||||||
subtitle: Row(
|
backgroundColor: Theme.of(
|
||||||
spacing: 6,
|
context,
|
||||||
children: [
|
).colorScheme.primary,
|
||||||
if (invite.chatRoom!.type == 1)
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
Badge(
|
|
||||||
label: const Text('directMessage').tr(),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
textColor:
|
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
trailing: Row(
|
],
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.check),
|
|
||||||
onPressed: () => acceptInvite(invite),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.close),
|
|
||||||
onPressed: () => declineInvite(invite),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.check),
|
||||||
|
onPressed: () => acceptInvite(invite),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
onPressed: () => declineInvite(invite),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -98,11 +98,10 @@ 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();
|
|
||||||
|
|
||||||
return members;
|
return members;
|
||||||
}
|
}
|
||||||
@@ -173,14 +172,12 @@ 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: isWideScreen(context)
|
||||||
iconDisabledColor:
|
? null
|
||||||
isWideScreen(context)
|
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
? null
|
|
||||||
: Theme.of(context).appBarTheme.foregroundColor!,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -204,16 +201,24 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (!hasPublishers) ...[
|
if (!hasPublishers) ...[
|
||||||
const Icon(
|
if (publishers.isLoading)
|
||||||
Symbols.info,
|
Padding(
|
||||||
fill: 1,
|
padding: const EdgeInsets.all(8),
|
||||||
size: 32,
|
child: const CircularProgressIndicator(),
|
||||||
).padding(bottom: 6, top: 24),
|
)
|
||||||
Text(
|
else
|
||||||
'creatorHubUnselectedHint',
|
...([
|
||||||
textAlign: TextAlign.center,
|
const Icon(
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
Symbols.info,
|
||||||
).tr(),
|
fill: 1,
|
||||||
|
size: 32,
|
||||||
|
).padding(bottom: 6, top: 24),
|
||||||
|
Text(
|
||||||
|
'creatorHubUnselectedHint',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
).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,29 +320,26 @@ class CreatorHubScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
||||||
data:
|
data: (data) => data
|
||||||
(data) =>
|
.map(
|
||||||
data
|
(item) => DropdownMenuItem<SnPublisher>(
|
||||||
.map(
|
value: item,
|
||||||
(item) => DropdownMenuItem<SnPublisher>(
|
child: ListTile(
|
||||||
value: item,
|
minTileHeight: 48,
|
||||||
child: ListTile(
|
leading: ProfilePictureWidget(
|
||||||
minTileHeight: 48,
|
radius: 16,
|
||||||
leading: ProfilePictureWidget(
|
fileId: item.picture?.id,
|
||||||
radius: 16,
|
),
|
||||||
fileId: item.picture?.id,
|
title: Text(item.nick),
|
||||||
),
|
subtitle: Text('@${item.name}'),
|
||||||
title: Text(item.nick),
|
trailing: currentPublisher.value?.id == item.id
|
||||||
subtitle: Text('@${item.name}'),
|
? const Icon(Icons.check)
|
||||||
trailing:
|
: null,
|
||||||
currentPublisher.value?.id == item.id
|
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
? const Icon(Icons.check)
|
),
|
||||||
: null,
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
)
|
||||||
),
|
.toList(),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
loading: () => [],
|
loading: () => [],
|
||||||
error: (_, _) => [],
|
error: (_, _) => [],
|
||||||
);
|
);
|
||||||
@@ -443,10 +445,9 @@ 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,51 +568,49 @@ 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: currentPublisher.value == null
|
||||||
child:
|
? ConstrainedBox(
|
||||||
currentPublisher.value == null
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
? ConstrainedBox(
|
child: _PublisherUnselectedWidget(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
onPublisherSelected: (publisher) {
|
||||||
child: _PublisherUnselectedWidget(
|
currentPublisher.value = publisher;
|
||||||
onPublisherSelected: (publisher) {
|
},
|
||||||
currentPublisher.value = publisher;
|
),
|
||||||
},
|
).center()
|
||||||
),
|
: isWide
|
||||||
).center()
|
? Column(
|
||||||
: isWide
|
spacing: 8,
|
||||||
? Column(
|
children: [
|
||||||
spacing: 8,
|
const SizedBox.shrink(),
|
||||||
children: [
|
PublisherSelector(
|
||||||
const SizedBox.shrink(),
|
currentPublisher: currentPublisher.value,
|
||||||
PublisherSelector(
|
publishersMenu: publishersMenu,
|
||||||
currentPublisher: currentPublisher.value,
|
onChanged: (value) {
|
||||||
publishersMenu: publishersMenu,
|
currentPublisher.value = value;
|
||||||
onChanged: (value) {
|
},
|
||||||
currentPublisher.value = value;
|
),
|
||||||
},
|
if (stats != null)
|
||||||
),
|
_PublisherStatsWidget(
|
||||||
if (stats != null)
|
stats: stats,
|
||||||
_PublisherStatsWidget(
|
heatmap: publisherHeatmap.value,
|
||||||
stats: stats,
|
).padding(horizontal: 12),
|
||||||
heatmap: publisherHeatmap.value,
|
buildNavigationWidget(true),
|
||||||
).padding(horizontal: 12),
|
],
|
||||||
buildNavigationWidget(true),
|
)
|
||||||
],
|
: Column(
|
||||||
)
|
spacing: 12,
|
||||||
: Column(
|
children: [
|
||||||
spacing: 12,
|
if (stats != null)
|
||||||
children: [
|
_PublisherStatsWidget(
|
||||||
if (stats != null)
|
stats: stats,
|
||||||
_PublisherStatsWidget(
|
heatmap: publisherHeatmap.value,
|
||||||
stats: stats,
|
).padding(horizontal: 16),
|
||||||
heatmap: publisherHeatmap.value,
|
buildNavigationWidget(false),
|
||||||
).padding(horizontal: 16),
|
],
|
||||||
buildNavigationWidget(false),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
@@ -876,11 +875,10 @@ 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,
|
),
|
||||||
),
|
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
memberNotifier.refresh();
|
memberNotifier.refresh();
|
||||||
@@ -991,23 +989,19 @@ 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,
|
return TextField(
|
||||||
focusNode,
|
controller: controller,
|
||||||
onFieldSubmitted,
|
focusNode: focusNode,
|
||||||
) {
|
keyboardType: TextInputType.number,
|
||||||
return TextField(
|
decoration: InputDecoration(
|
||||||
controller: controller,
|
labelText: 'memberRole'.tr(),
|
||||||
focusNode: focusNode,
|
helperText: 'memberRoleHint'.tr(),
|
||||||
keyboardType: TextInputType.number,
|
),
|
||||||
decoration: InputDecoration(
|
onTapOutside: (event) => focusNode.unfocus(),
|
||||||
labelText: 'memberRole'.tr(),
|
);
|
||||||
helperText: 'memberRoleHint'.tr(),
|
},
|
||||||
),
|
|
||||||
onTapOutside: (event) => focusNode.unfocus(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
@@ -1085,57 +1079,49 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: invites.when(
|
child: invites.when(
|
||||||
data:
|
data: (items) => items.isEmpty
|
||||||
(items) =>
|
? Center(
|
||||||
items.isEmpty
|
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
|
||||||
? Center(
|
)
|
||||||
child:
|
: ListView.builder(
|
||||||
Text(
|
shrinkWrap: true,
|
||||||
'invitesEmpty',
|
itemCount: items.length,
|
||||||
textAlign: TextAlign.center,
|
itemBuilder: (context, index) {
|
||||||
).tr(),
|
final invite = items[index];
|
||||||
)
|
return ListTile(
|
||||||
: ListView.builder(
|
leading: ProfilePictureWidget(
|
||||||
shrinkWrap: true,
|
fileId: invite.publisher!.picture?.id,
|
||||||
itemCount: items.length,
|
fallbackIcon: Symbols.group,
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final invite = items[index];
|
|
||||||
return ListTile(
|
|
||||||
leading: ProfilePictureWidget(
|
|
||||||
fileId: invite.publisher!.picture?.id,
|
|
||||||
fallbackIcon: Symbols.group,
|
|
||||||
),
|
|
||||||
title: Text(invite.publisher!.nick),
|
|
||||||
subtitle:
|
|
||||||
Text(
|
|
||||||
invite.role >= 100
|
|
||||||
? 'permissionOwner'
|
|
||||||
: invite.role >= 50
|
|
||||||
? 'permissionModerator'
|
|
||||||
: 'permissionMember',
|
|
||||||
).tr(),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.check),
|
|
||||||
onPressed: () => acceptInvite(invite),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.close),
|
|
||||||
onPressed: () => declineInvite(invite),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
title: Text(invite.publisher!.nick),
|
||||||
|
subtitle: Text(
|
||||||
|
invite.role >= 100
|
||||||
|
? 'permissionOwner'
|
||||||
|
: invite.role >= 50
|
||||||
|
? 'permissionModerator'
|
||||||
|
: 'permissionMember',
|
||||||
|
).tr(),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.check),
|
||||||
|
onPressed: () => acceptInvite(invite),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.close),
|
||||||
|
onPressed: () => declineInvite(invite),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
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),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,10 @@ 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();
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -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,10 +119,9 @@ 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);
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
@@ -152,78 +151,69 @@ class _CreatorPollItem extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: PopupMenuButton<String>(
|
trailing: PopupMenuButton<String>(
|
||||||
itemBuilder:
|
itemBuilder: (context) => [
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.edit),
|
||||||
const Icon(Symbols.edit),
|
const Gap(16),
|
||||||
const Gap(16),
|
Text('edit').tr(),
|
||||||
Text('edit').tr(),
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final result = await showModalBottomSheet<SnPoll>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
isDismissible: false,
|
||||||
|
builder: (context) => PollEditorScreen(
|
||||||
|
initialPublisher: pubName,
|
||||||
|
initialPollId: pollWithStats.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr().textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Delete Poll'),
|
||||||
|
content: Text('Are you sure you want to delete this poll?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text('Delete'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () async {
|
);
|
||||||
final result = await showModalBottomSheet<SnPoll>(
|
if (confirmed == true) {
|
||||||
context: context,
|
try {
|
||||||
isScrollControlled: true,
|
final client = ref.read(apiClientProvider);
|
||||||
isDismissible: false,
|
await client.delete('/sphere/polls/${pollWithStats.id}');
|
||||||
builder:
|
ref.invalidate(pollListNotifierProvider(pubName));
|
||||||
(context) => PollEditorScreen(
|
showSnackBar('Poll deleted successfully');
|
||||||
initialPublisher: pubName,
|
} catch (e) {
|
||||||
initialPollId: pollWithStats.id,
|
showErrorAlert(e);
|
||||||
),
|
}
|
||||||
);
|
}
|
||||||
if (result != null && context.mounted) {
|
},
|
||||||
ref.invalidate(pollListNotifierProvider(pubName));
|
),
|
||||||
}
|
],
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
|
||||||
const Gap(16),
|
|
||||||
Text('delete').tr().textColor(Colors.red),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: Text('Delete Poll'),
|
|
||||||
content: Text(
|
|
||||||
'Are you sure you want to delete this poll?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
() => Navigator.of(context).pop(false),
|
|
||||||
child: Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed:
|
|
||||||
() => Navigator.of(context).pop(true),
|
|
||||||
child: Text('Delete'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (confirmed == true) {
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/sphere/polls/${pollWithStats.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(pollListNotifierProvider(pubName));
|
|
||||||
showSnackBar('Poll deleted successfully');
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,11 +37,10 @@ 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();
|
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -70,23 +68,17 @@ 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(
|
provider: siteListNotifierProvider(pubName),
|
||||||
slivers: [
|
notifier: siteListNotifierProvider(pubName).notifier,
|
||||||
const SliverGap(8),
|
padding: const EdgeInsets.only(top: 12),
|
||||||
PaginationList(
|
itemBuilder: (context, index, site) {
|
||||||
provider: siteListNotifierProvider(pubName),
|
return ConstrainedBox(
|
||||||
notifier: siteListNotifierProvider(pubName).notifier,
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
itemBuilder: (context, index, site) {
|
child: _CreatorSiteItem(site: site, pubName: pubName),
|
||||||
return ConstrainedBox(
|
).center();
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
},
|
||||||
child: _CreatorSiteItem(site: site, pubName: pubName),
|
|
||||||
).center();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,73 +140,53 @@ class _CreatorSiteItem extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
itemBuilder:
|
itemBuilder: (context) => [
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.edit),
|
||||||
const Icon(Symbols.edit),
|
const Gap(16),
|
||||||
const Gap(16),
|
Text('edit').tr(),
|
||||||
Text('edit').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
showModalBottomSheet(
|
||||||
showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
isScrollControlled: true,
|
||||||
isScrollControlled: true,
|
builder: (context) =>
|
||||||
builder:
|
SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||||
(context) => SiteForm(
|
);
|
||||||
pubName: pubName,
|
},
|
||||||
siteSlug: site.slug,
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
|
const Gap(16),
|
||||||
|
Text('delete').tr().textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'publicationSiteDeleteConfirm'.tr(),
|
||||||
|
'deleteSite'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
);
|
||||||
|
if (confirmed == true) {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/zone/sites/$pubName/${site.slug}',
|
||||||
);
|
);
|
||||||
},
|
ref.invalidate(siteListNotifierProvider(pubName));
|
||||||
),
|
showSnackBar('siteDeletedSuccess'.tr());
|
||||||
PopupMenuItem(
|
} catch (e) {
|
||||||
child: Row(
|
showErrorAlert(e);
|
||||||
children: [
|
}
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
}
|
||||||
const Gap(16),
|
},
|
||||||
Text('delete').tr().textColor(Colors.red),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
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) {
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/zone/sites/$pubName/${site.slug}',
|
|
||||||
);
|
|
||||||
ref.invalidate(siteListNotifierProvider(pubName));
|
|
||||||
showSnackBar('siteDeletedSuccess'.tr());
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ 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),
|
),
|
||||||
),
|
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(stickerPacksProvider(pubName));
|
ref.invalidate(stickerPacksProvider(pubName));
|
||||||
@@ -54,24 +53,23 @@ 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),
|
child: Card(
|
||||||
child: Card(
|
shape: RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: const BorderRadius.only(
|
||||||
borderRadius: const BorderRadius.only(
|
topLeft: Radius.circular(8),
|
||||||
topLeft: Radius.circular(8),
|
topRight: Radius.circular(8),
|
||||||
topRight: Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.only(top: 16),
|
|
||||||
child: content,
|
|
||||||
),
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 16),
|
||||||
|
child: content,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: content,
|
)
|
||||||
|
: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,40 +96,38 @@ 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(
|
icon: const Icon(Symbols.add_circle),
|
||||||
icon: const Icon(Symbols.add_circle),
|
onPressed: () {
|
||||||
onPressed: () {
|
final id = sticker.id;
|
||||||
final id = sticker.id;
|
showModalBottomSheet(
|
||||||
showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
isScrollControlled: true,
|
||||||
isScrollControlled: true,
|
builder: (context) => SheetScaffold(
|
||||||
builder:
|
titleText: 'createSticker'.tr(),
|
||||||
(context) => SheetScaffold(
|
child: StickerForm(packId: id),
|
||||||
titleText: 'createSticker'.tr(),
|
),
|
||||||
child: StickerForm(packId: id),
|
).then((value) {
|
||||||
),
|
if (value != null) {
|
||||||
).then((value) {
|
ref.invalidate(stickerPackContentProvider(id));
|
||||||
if (value != null) {
|
}
|
||||||
ref.invalidate(stickerPackContentProvider(id));
|
});
|
||||||
}
|
},
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
StickerPackActionMenu(
|
|
||||||
pubName: pubName,
|
|
||||||
packId: sticker.id,
|
|
||||||
iconShadow: Shadow(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: StickerPackDetailContent(
|
|
||||||
id: sticker.id,
|
|
||||||
pubName: pubName,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
StickerPackActionMenu(
|
||||||
|
pubName: pubName,
|
||||||
|
packId: sticker.id,
|
||||||
|
iconShadow: Shadow(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: StickerPackDetailContent(
|
||||||
|
id: sticker.id,
|
||||||
|
pubName: pubName,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -165,11 +162,10 @@ 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();
|
|
||||||
|
|
||||||
return stickers;
|
return stickers;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -262,10 +258,9 @@ 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,10 +268,9 @@ 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) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
icon.value = value[0].id;
|
icon.value = value[0].id;
|
||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -39,19 +39,18 @@ 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(
|
'developerAppEdit',
|
||||||
'developerAppEdit',
|
pathParameters: {
|
||||||
pathParameters: {
|
'name': publisherName,
|
||||||
'name': publisherName,
|
'projectId': projectId,
|
||||||
'projectId': projectId,
|
'id': appId,
|
||||||
'id': appId,
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
@@ -85,24 +84,34 @@ class AppDetailScreen extends HookConsumerWidget {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
_AppOverview(app: app),
|
Align(
|
||||||
AppSecretsScreen(
|
alignment: Alignment.topCenter,
|
||||||
publisherName: publisherName,
|
child: ConstrainedBox(
|
||||||
projectId: projectId,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
appId: appId,
|
child: _AppOverview(app: app),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
child: AppSecretsScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
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: () => ref.invalidate(
|
||||||
onRetry:
|
customAppProvider(publisherName, projectId, appId),
|
||||||
() => ref.invalidate(
|
),
|
||||||
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,13 +135,12 @@ 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,
|
)
|
||||||
)
|
: const SizedBox.shrink(),
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|||||||
@@ -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,37 +54,36 @@ 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),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Text('copySecretHint'.tr()),
|
||||||
Text('copySecretHint'.tr()),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
Container(
|
||||||
Container(
|
padding: const EdgeInsets.all(12),
|
||||||
padding: const EdgeInsets.all(12),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderRadius: BorderRadius.circular(8),
|
),
|
||||||
),
|
child: SelectableText(newSecret),
|
||||||
child: SelectableText(newSecret),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: newSecret));
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.copy_all),
|
|
||||||
label: Text('copy'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: newSecret));
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.copy_all),
|
||||||
|
label: Text('copy'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
).whenComplete(() {
|
).whenComplete(() {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
customAppSecretsProvider(publisherName, projectId, appId),
|
customAppSecretsProvider(publisherName, projectId, appId),
|
||||||
@@ -114,22 +114,38 @@ 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(
|
||||||
title: Text('isOidc'.tr()),
|
margin: EdgeInsets.zero,
|
||||||
value: isOidc.value,
|
child: SwitchListTile(
|
||||||
onChanged: (value) => isOidc.value = value,
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
title: Text('isOidc'.tr()),
|
||||||
|
value: isOidc.value,
|
||||||
|
onChanged: (value) => isOidc.value = value,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
@@ -175,14 +191,9 @@ 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: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -240,14 +251,12 @@ 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: () => ref.invalidate(
|
||||||
onRetry:
|
customAppSecretsProvider(publisherName, projectId, appId),
|
||||||
() => ref.invalidate(
|
),
|
||||||
customAppSecretsProvider(publisherName, projectId, appId),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,15 +76,14 @@ 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,
|
projectId: projectId,
|
||||||
projectId: projectId,
|
isModal: true,
|
||||||
isModal: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -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,15 +107,14 @@ 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,
|
projectId: projectId,
|
||||||
projectId: projectId,
|
isModal: true,
|
||||||
isModal: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -146,31 +142,20 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
if (app.background != null)
|
||||||
height: 150,
|
AspectRatio(
|
||||||
child: Stack(
|
aspectRatio: 16 / 7,
|
||||||
fit: StackFit.expand,
|
child: CloudFileWidget(
|
||||||
children: [
|
item: app.background!,
|
||||||
if (app.background != null)
|
fit: BoxFit.cover,
|
||||||
CloudFileWidget(
|
).clipRRect(topLeft: 8, topRight: 8),
|
||||||
item: app.background!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
).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,52 +165,48 @@ 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(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.edit),
|
||||||
const Icon(Symbols.edit),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(width: 12),
|
Text('edit').tr(),
|
||||||
Text('edit').tr(),
|
],
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.delete,
|
||||||
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
PopupMenuItem(
|
Text(
|
||||||
value: 'delete',
|
'delete',
|
||||||
child: Row(
|
style: TextStyle(color: Colors.red),
|
||||||
children: [
|
).tr(),
|
||||||
const Icon(
|
],
|
||||||
Symbols.delete,
|
),
|
||||||
color: Colors.red,
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
'delete',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'edit') {
|
if (value == 'edit') {
|
||||||
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,
|
projectId: projectId,
|
||||||
projectId: projectId,
|
id: app.id,
|
||||||
id: app.id,
|
isModal: true,
|
||||||
isModal: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (value == 'delete') {
|
} else if (value == 'delete') {
|
||||||
showConfirmAlert(
|
showConfirmAlert(
|
||||||
@@ -264,14 +245,11 @@ 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(customAppsProvider(publisherName, projectId)),
|
||||||
() => ref.invalidate(
|
),
|
||||||
customAppsProvider(publisherName, projectId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,18 @@ 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(
|
'developerBotEdit',
|
||||||
'developerBotEdit',
|
pathParameters: {
|
||||||
pathParameters: {
|
'name': publisherName,
|
||||||
'name': publisherName,
|
'projectId': projectId,
|
||||||
'projectId': projectId,
|
'id': botId,
|
||||||
'id': botId,
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
@@ -84,24 +83,33 @@ class BotDetailScreen extends HookConsumerWidget {
|
|||||||
controller: tabController,
|
controller: tabController,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
_BotOverview(bot: bot),
|
Align(
|
||||||
BotKeysScreen(
|
alignment: Alignment.topCenter,
|
||||||
publisherName: publisherName,
|
child: ConstrainedBox(
|
||||||
projectId: projectId,
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
botId: botId,
|
child: _BotOverview(bot: bot),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
child: BotKeysScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
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(botProvider(publisherName, projectId, botId)),
|
||||||
() => ref.invalidate(
|
),
|
||||||
botProvider(publisherName, projectId, botId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -124,13 +132,12 @@ 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,
|
)
|
||||||
)
|
: const SizedBox.shrink(),
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 20,
|
left: 20,
|
||||||
|
|||||||
@@ -53,37 +53,36 @@ 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),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Text('copyKeyHint'.tr()),
|
||||||
Text('copyKeyHint'.tr()),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
Container(
|
||||||
Container(
|
padding: const EdgeInsets.all(12),
|
||||||
padding: const EdgeInsets.all(12),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderRadius: BorderRadius.circular(8),
|
),
|
||||||
),
|
child: SelectableText(token),
|
||||||
child: SelectableText(token),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Clipboard.setData(ClipboardData(text: token));
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.copy_all),
|
|
||||||
label: Text('copy'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: token));
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.copy_all),
|
||||||
|
label: Text('copy'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
).whenComplete(() {
|
).whenComplete(() {
|
||||||
ref.invalidate(botKeysProvider(publisherName, projectId, botId));
|
ref.invalidate(botKeysProvider(publisherName, projectId, botId));
|
||||||
});
|
});
|
||||||
@@ -94,45 +93,50 @@ 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),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: keyNameController,
|
controller: keyNameController,
|
||||||
decoration: InputDecoration(labelText: 'keyName'.tr()),
|
decoration: InputDecoration(
|
||||||
autofocus: true,
|
labelText: 'keyName'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
FilledButton.icon(
|
autofocus: true,
|
||||||
onPressed: () async {
|
|
||||||
if (keyNameController.text.isEmpty) return;
|
|
||||||
final keyName = keyNameController.text;
|
|
||||||
Navigator.pop(context); // Close the sheet
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final resp = await client.post(
|
|
||||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
|
||||||
data: {'label': keyName},
|
|
||||||
);
|
|
||||||
final newApiKey = SnAccountApiKey.fromJson(resp.data);
|
|
||||||
showNewKeySheet(newApiKey);
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.add),
|
|
||||||
label: Text('create'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
if (keyNameController.text.isEmpty) return;
|
||||||
|
final keyName = keyNameController.text;
|
||||||
|
Navigator.pop(context); // Close the sheet
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.post(
|
||||||
|
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
||||||
|
data: {'label': keyName},
|
||||||
|
);
|
||||||
|
final newApiKey = SnAccountApiKey.fromJson(resp.data);
|
||||||
|
showNewKeySheet(newApiKey);
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
label: Text('create'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,92 +193,79 @@ 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: () => ref.refresh(
|
||||||
onRefresh:
|
botKeysProvider(publisherName, projectId, botId).future,
|
||||||
() => ref.refresh(
|
),
|
||||||
botKeysProvider(
|
child: ListView.builder(
|
||||||
publisherName,
|
padding: EdgeInsets.zero,
|
||||||
projectId,
|
itemCount: data.length,
|
||||||
botId,
|
itemBuilder: (context, index) {
|
||||||
).future,
|
final apiKey = data[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(apiKey.label),
|
||||||
|
subtitle: Text(apiKey.createdAt.formatSystem()),
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 12,
|
||||||
),
|
),
|
||||||
child: ListView.builder(
|
trailing: PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
itemBuilder: (context) => [
|
||||||
itemCount: data.length,
|
PopupMenuItem(
|
||||||
itemBuilder: (context, index) {
|
value: 'rotate',
|
||||||
final apiKey = data[index];
|
child: Row(
|
||||||
return ListTile(
|
children: [
|
||||||
title: Text(apiKey.label),
|
const Icon(Symbols.refresh),
|
||||||
subtitle: Text(apiKey.createdAt.formatSystem()),
|
const Gap(12),
|
||||||
contentPadding: EdgeInsets.only(
|
Text('rotateKey'.tr()),
|
||||||
left: 16,
|
],
|
||||||
right: 12,
|
),
|
||||||
),
|
),
|
||||||
trailing: PopupMenuButton(
|
PopupMenuItem(
|
||||||
itemBuilder:
|
value: 'revoke',
|
||||||
(context) => [
|
child: Row(
|
||||||
PopupMenuItem(
|
children: [
|
||||||
value: 'rotate',
|
const Icon(
|
||||||
child: Row(
|
Symbols.delete,
|
||||||
children: [
|
color: Colors.red,
|
||||||
const Icon(Symbols.refresh),
|
|
||||||
const Gap(12),
|
|
||||||
Text('rotateKey'.tr()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
const Gap(12),
|
||||||
value: 'revoke',
|
Text(
|
||||||
child: Row(
|
'revoke'.tr(),
|
||||||
children: [
|
style: TextStyle(color: Colors.red),
|
||||||
const Icon(
|
|
||||||
Symbols.delete,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Text(
|
|
||||||
'revoke'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
),
|
||||||
if (value == 'rotate') {
|
),
|
||||||
rotateKey(apiKey.id);
|
],
|
||||||
} else if (value == 'revoke') {
|
onSelected: (value) {
|
||||||
revokeKey(apiKey.id);
|
if (value == 'rotate') {
|
||||||
}
|
rotateKey(apiKey.id);
|
||||||
},
|
} else if (value == 'revoke') {
|
||||||
),
|
revokeKey(apiKey.id);
|
||||||
);
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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(botKeysProvider(publisherName, projectId, botId)),
|
||||||
() => ref.invalidate(
|
),
|
||||||
botKeysProvider(publisherName, projectId, botId),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,14 @@ 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,
|
projectId: projectId,
|
||||||
projectId: projectId,
|
isModal: true,
|
||||||
isModal: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -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,15 +85,14 @@ 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,
|
projectId: projectId,
|
||||||
projectId: projectId,
|
isModal: true,
|
||||||
isModal: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -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(
|
||||||
leading: CircleAvatar(
|
aspectRatio: 16 / 7,
|
||||||
child:
|
child: CloudFileWidget(
|
||||||
bot.account.profile.picture != null
|
item: bot.account.profile.background!,
|
||||||
? ProfilePictureWidget(
|
fit: BoxFit.cover,
|
||||||
file: bot.account.profile.picture!,
|
).clipRRect(topLeft: 8, topRight: 8),
|
||||||
)
|
),
|
||||||
: const Icon(Symbols.smart_toy),
|
ListTile(
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
title: Text(bot.account.nick),
|
borderRadius: BorderRadius.all(
|
||||||
subtitle: Text(bot.account.name),
|
Radius.circular(8.0),
|
||||||
trailing: PopupMenuButton(
|
),
|
||||||
itemBuilder:
|
),
|
||||||
(context) => [
|
leading: ProfilePictureWidget(
|
||||||
|
fallbackIcon: Symbols.smart_toy,
|
||||||
|
file: bot.account.profile.picture,
|
||||||
|
),
|
||||||
|
title: Text(bot.account.nick),
|
||||||
|
subtitle: Text(bot.account.name),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'edit',
|
value: 'edit',
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -152,13 +157,12 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'edit') {
|
if (value == 'edit') {
|
||||||
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,
|
||||||
@@ -167,36 +171,40 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
isModal: true,
|
isModal: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
} else if (value == 'delete') {
|
|
||||||
showConfirmAlert(
|
|
||||||
'deleteBotHint'.tr(),
|
|
||||||
'deleteBot'.tr(),
|
|
||||||
isDanger: true,
|
|
||||||
).then((confirm) {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
client.delete(
|
|
||||||
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(
|
|
||||||
botsProvider(publisherName, projectId),
|
|
||||||
);
|
);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
showConfirmAlert(
|
||||||
|
'deleteBotHint'.tr(),
|
||||||
|
'deleteBot'.tr(),
|
||||||
|
isDanger: true,
|
||||||
|
).then((confirm) {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.read(
|
||||||
|
apiClientProvider,
|
||||||
|
);
|
||||||
|
client.delete(
|
||||||
|
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
||||||
|
);
|
||||||
|
ref.invalidate(
|
||||||
|
botsProvider(publisherName, projectId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
),
|
||||||
},
|
onTap: () {
|
||||||
),
|
context.pushNamed(
|
||||||
onTap: () {
|
'developerBotDetail',
|
||||||
context.pushNamed(
|
pathParameters: {
|
||||||
'developerBotDetail',
|
'name': publisherName,
|
||||||
pathParameters: {
|
'projectId': projectId,
|
||||||
'name': publisherName,
|
'botId': bot.id,
|
||||||
'projectId': projectId,
|
},
|
||||||
'botId': bot.id,
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -207,12 +215,10 @@ 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: () => ref.invalidate(botsProvider(publisherName, projectId)),
|
||||||
onRetry:
|
),
|
||||||
() => ref.invalidate(botsProvider(publisherName, projectId)),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,11 @@ 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),
|
)
|
||||||
)
|
: const AsyncValue<List<DevProject>>.data([]);
|
||||||
: const AsyncValue<List<DevProject>>.data([]);
|
|
||||||
|
|
||||||
final currentProject = useState<DevProject?>(
|
final currentProject = useState<DevProject?>(
|
||||||
projects.value?.where((p) => p.id == initialProjectId).firstOrNull,
|
projects.value?.where((p) => p.id == initialProjectId).firstOrNull,
|
||||||
@@ -126,14 +125,13 @@ 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:
|
currentDeveloper.value!.publisher!.name,
|
||||||
currentDeveloper.value!.publisher!.name,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(
|
ref.invalidate(
|
||||||
@@ -211,108 +209,96 @@ 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) =>
|
? ConstrainedBox(
|
||||||
currentDeveloper == null
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
? ConstrainedBox(
|
child: _DeveloperUnselectedWidget(
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
onDeveloperSelected: onDeveloperSelected,
|
||||||
child: _DeveloperUnselectedWidget(
|
),
|
||||||
onDeveloperSelected: onDeveloperSelected,
|
).center()
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Developer Stats
|
||||||
|
if (stats != null) ...[
|
||||||
|
Text(
|
||||||
|
'Overview',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
).center()
|
const Gap(16),
|
||||||
: Padding(
|
_DeveloperStatsWidget(stats: stats),
|
||||||
padding: const EdgeInsets.all(16),
|
const Gap(24),
|
||||||
child: Column(
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
// Projects Section
|
||||||
// Developer Stats
|
Row(
|
||||||
if (stats != null) ...[
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Overview',
|
'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(),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: onCreateProject,
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
label: const Text('Create Project'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF1A73E8),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
const Gap(16),
|
|
||||||
_DeveloperStatsWidget(stats: stats),
|
|
||||||
const Gap(24),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Projects Section
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Projects',
|
|
||||||
style: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.titleLarge?.copyWith(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: onCreateProject,
|
|
||||||
icon: const Icon(Symbols.add),
|
|
||||||
label: const Text('Create Project'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFF1A73E8),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const Gap(16),
|
),
|
||||||
|
],
|
||||||
// Projects List
|
|
||||||
projects.value?.isNotEmpty ?? false
|
|
||||||
? Column(
|
|
||||||
children:
|
|
||||||
projects.value!
|
|
||||||
.map(
|
|
||||||
(project) => _ProjectListTile(
|
|
||||||
project: project,
|
|
||||||
publisherName:
|
|
||||||
currentDeveloper!
|
|
||||||
.publisher!
|
|
||||||
.name,
|
|
||||||
onProjectSelected:
|
|
||||||
onProjectSelected,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.all(48),
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
'No projects available',
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onSurface,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const Gap(16),
|
||||||
|
|
||||||
|
// Projects List
|
||||||
|
projects.value?.isNotEmpty ?? false
|
||||||
|
? Column(
|
||||||
|
children: projects.value!
|
||||||
|
.map(
|
||||||
|
(project) => _ProjectListTile(
|
||||||
|
project: project,
|
||||||
|
publisherName:
|
||||||
|
currentDeveloper!.publisher!.name,
|
||||||
|
onProjectSelected: onProjectSelected,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.all(48),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'No projects available',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
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(
|
developerStatsProvider(currentDeveloper?.publisher?.name),
|
||||||
developerStatsProvider(currentDeveloper?.publisher?.name),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -335,29 +321,26 @@ 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) =>
|
.map(
|
||||||
data
|
(item) => DropdownMenuItem<SnDeveloper>(
|
||||||
.map(
|
value: item,
|
||||||
(item) => DropdownMenuItem<SnDeveloper>(
|
child: ListTile(
|
||||||
value: item,
|
minTileHeight: 48,
|
||||||
child: ListTile(
|
leading: ProfilePictureWidget(
|
||||||
minTileHeight: 48,
|
radius: 16,
|
||||||
leading: ProfilePictureWidget(
|
fileId: item.publisher?.picture?.id,
|
||||||
radius: 16,
|
),
|
||||||
fileId: item.publisher?.picture?.id,
|
title: Text(item.publisher!.nick),
|
||||||
),
|
subtitle: Text('@${item.publisher!.name}'),
|
||||||
title: Text(item.publisher!.nick),
|
trailing: currentDeveloper?.id == item.id
|
||||||
subtitle: Text('@${item.publisher!.name}'),
|
? const Icon(Icons.check)
|
||||||
trailing:
|
: null,
|
||||||
currentDeveloper?.id == item.id
|
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
? const Icon(Icons.check)
|
),
|
||||||
: null,
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
)
|
||||||
),
|
.toList(),
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
loading: () => [],
|
loading: () => [],
|
||||||
error: (_, _) => [],
|
error: (_, _) => [],
|
||||||
);
|
);
|
||||||
@@ -446,38 +429,36 @@ 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,
|
child: ListTile(
|
||||||
child: ListTile(
|
minTileHeight: 48,
|
||||||
minTileHeight: 48,
|
leading: CircleAvatar(
|
||||||
leading: CircleAvatar(
|
radius: 16,
|
||||||
radius: 16,
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
child: Text(
|
||||||
child: Text(
|
item.name.isNotEmpty ? item.name[0].toUpperCase() : '?',
|
||||||
item.name.isNotEmpty ? item.name[0].toUpperCase() : '?',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
title: Text(item.name),
|
|
||||||
subtitle: Text(
|
|
||||||
item.description ?? '',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
trailing:
|
|
||||||
currentProject?.id == item.id
|
|
||||||
? const Icon(Icons.check)
|
|
||||||
: null,
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
title: Text(item.name),
|
||||||
.toList();
|
subtitle: Text(
|
||||||
|
item.description ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
trailing: currentProject?.id == item.id
|
||||||
|
? const Icon(Icons.check)
|
||||||
|
: null,
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return DropdownButtonHideUnderline(
|
return DropdownButtonHideUnderline(
|
||||||
child: DropdownButton2<DevProject>(
|
child: DropdownButton2<DevProject>(
|
||||||
@@ -496,50 +477,47 @@ 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: Theme.of(
|
||||||
backgroundColor:
|
context,
|
||||||
Theme.of(context).colorScheme.primary,
|
).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(
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Text(
|
|
||||||
e.value?.name ?? '?',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color:
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.foregroundColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
).padding(right: 8)
|
|
||||||
: CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
child: Text(
|
|
||||||
e.value?.name.isNotEmpty ?? false
|
|
||||||
? e.value!.name[0].toUpperCase()
|
|
||||||
: '?',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).center().padding(right: 8),
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
e.value?.name ?? '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(right: 8)
|
||||||
|
: CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
child: Text(
|
||||||
|
e.value?.name.isNotEmpty ?? false
|
||||||
|
? e.value!.name[0].toUpperCase()
|
||||||
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).center().padding(right: 8),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
@@ -590,45 +568,40 @@ 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(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.edit),
|
||||||
const Icon(Symbols.edit),
|
const SizedBox(width: 12),
|
||||||
const SizedBox(width: 12),
|
Text('edit').tr(),
|
||||||
Text('edit').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
value: 'delete',
|
||||||
value: 'delete',
|
child: Row(
|
||||||
child: Row(
|
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('delete', style: const TextStyle(color: Colors.red)).tr(),
|
||||||
Text(
|
],
|
||||||
'delete',
|
),
|
||||||
style: const TextStyle(color: Colors.red),
|
),
|
||||||
).tr(),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
if (value == 'edit') {
|
if (value == 'edit') {
|
||||||
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,
|
project: project,
|
||||||
project: project,
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
ref.invalidate(devProjectsProvider(publisherName));
|
ref.invalidate(devProjectsProvider(publisherName));
|
||||||
@@ -735,16 +708,24 @@ class _DeveloperUnselectedWidget extends HookConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (!hasDevelopers) ...[
|
if (!hasDevelopers) ...[
|
||||||
const Icon(
|
if (developers.isLoading)
|
||||||
Symbols.info,
|
Padding(
|
||||||
fill: 1,
|
padding: const EdgeInsets.all(8),
|
||||||
size: 32,
|
child: const CircularProgressIndicator(),
|
||||||
).padding(bottom: 6, top: 24),
|
)
|
||||||
Text(
|
else
|
||||||
'developerHubUnselectedHint',
|
...([
|
||||||
textAlign: TextAlign.center,
|
const Icon(
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
Symbols.info,
|
||||||
).tr(),
|
fill: 1,
|
||||||
|
size: 32,
|
||||||
|
).padding(bottom: 6, top: 24),
|
||||||
|
Text(
|
||||||
|
'developerHubUnselectedHint',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
).tr(),
|
||||||
|
]),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
],
|
],
|
||||||
if (hasDevelopers)
|
if (hasDevelopers)
|
||||||
@@ -818,16 +799,15 @@ 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,
|
)
|
||||||
)
|
: await client.post(
|
||||||
: await client.post(
|
'/develop/developers/$publisherName/projects',
|
||||||
'/develop/developers/$publisherName/projects',
|
data: data,
|
||||||
data: data,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
Navigator.of(context).pop(DevProject.fromJson(resp.data));
|
Navigator.of(context).pop(DevProject.fromJson(resp.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,38 +914,34 @@ 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) =>
|
? Center(
|
||||||
items.isEmpty
|
child: Text(
|
||||||
? Center(
|
'noDevelopersToEnroll',
|
||||||
child:
|
textAlign: TextAlign.center,
|
||||||
Text(
|
).tr(),
|
||||||
'noDevelopersToEnroll',
|
)
|
||||||
textAlign: TextAlign.center,
|
: ListView.builder(
|
||||||
).tr(),
|
shrinkWrap: true,
|
||||||
)
|
itemCount: items.length,
|
||||||
: ListView.builder(
|
itemBuilder: (context, index) {
|
||||||
shrinkWrap: true,
|
final publisher = items[index];
|
||||||
itemCount: items.length,
|
return ListTile(
|
||||||
itemBuilder: (context, index) {
|
leading: ProfilePictureWidget(
|
||||||
final publisher = items[index];
|
fileId: publisher.picture?.id,
|
||||||
return ListTile(
|
fallbackIcon: Symbols.group,
|
||||||
leading: ProfilePictureWidget(
|
|
||||||
fileId: publisher.picture?.id,
|
|
||||||
fallbackIcon: Symbols.group,
|
|
||||||
),
|
|
||||||
title: Text(publisher.nick),
|
|
||||||
subtitle: Text('@${publisher.name}'),
|
|
||||||
onTap: () => enroll(publisher),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
title: Text(publisher.nick),
|
||||||
|
subtitle: Text('@${publisher.name}'),
|
||||||
|
onTap: () => enroll(publisher),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
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),
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,13 +46,10 @@ 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(
|
.cast<SnWebArticle>()
|
||||||
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
|
.toList();
|
||||||
)
|
|
||||||
.cast<SnWebArticle>()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
|
totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
|
||||||
|
|
||||||
@@ -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,18 +187,16 @@ 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: (err, stack) => AppScaffold(
|
||||||
error:
|
isNoBackground: false,
|
||||||
(err, stack) => AppScaffold(
|
appBar: AppBar(title: const Text('Articles')),
|
||||||
isNoBackground: false,
|
body: Center(child: Text('Error: $err')),
|
||||||
appBar: AppBar(title: const Text('Articles')),
|
),
|
||||||
body: Center(child: Text('Error: $err')),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,10 @@ 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();
|
|
||||||
|
|
||||||
return articles;
|
return articles;
|
||||||
}
|
}
|
||||||
@@ -116,31 +115,30 @@ 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: [
|
||||||
|
Text(data.description ?? 'descriptionNone'.tr()),
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(data.description ?? 'descriptionNone'.tr()),
|
const Icon(Symbols.rss_feed, size: 16),
|
||||||
Row(
|
Text(
|
||||||
spacing: 4,
|
'webFeedArticleCount'.plural(
|
||||||
children: [
|
feedNotifier.totalCount ?? 0,
|
||||||
const Icon(Symbols.rss_feed, size: 16),
|
),
|
||||||
Text(
|
),
|
||||||
'webFeedArticleCount'.plural(
|
|
||||||
feedNotifier.totalCount ?? 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).opacity(0.85),
|
|
||||||
Row(
|
|
||||||
spacing: 4,
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.link, size: 16),
|
|
||||||
SelectableText(data.url),
|
|
||||||
],
|
|
||||||
).opacity(0.85),
|
|
||||||
],
|
],
|
||||||
),
|
).opacity(0.85),
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.link, size: 16),
|
||||||
|
SelectableText(data.url),
|
||||||
|
],
|
||||||
|
).opacity(0.85),
|
||||||
|
],
|
||||||
|
),
|
||||||
error: (err, _) => Text(err.toString()),
|
error: (err, _) => Text(err.toString()),
|
||||||
loading: () => CircularProgressIndicator().center(),
|
loading: () => CircularProgressIndicator().center(),
|
||||||
)
|
)
|
||||||
@@ -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,29 +165,25 @@ 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:
|
icon: Icon(
|
||||||
isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
|
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
|
||||||
icon: Icon(
|
),
|
||||||
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
|
label: Text(
|
||||||
),
|
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
|
||||||
label: Text(
|
),
|
||||||
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
|
),
|
||||||
),
|
loading: () => const SizedBox(
|
||||||
),
|
height: 32,
|
||||||
loading:
|
width: 32,
|
||||||
() => const SizedBox(
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
height: 32,
|
).center(),
|
||||||
width: 32,
|
error: (_, _) => OutlinedButton.icon(
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
onPressed: subscribeToFeed,
|
||||||
),
|
icon: const Icon(Symbols.add_circle),
|
||||||
error:
|
label: Text('subscribe').tr(),
|
||||||
(_, _) => OutlinedButton.icon(
|
),
|
||||||
onPressed: subscribeToFeed,
|
|
||||||
icon: const Icon(Symbols.add_circle),
|
|
||||||
label: Text('subscribe').tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,11 +38,10 @@ 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();
|
|
||||||
|
|
||||||
return feeds;
|
return feeds;
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
|
|||||||
import 'package:island/widgets/paging/pagination_list.dart';
|
import 'package:island/widgets/paging/pagination_list.dart';
|
||||||
import 'package:island/widgets/post/post_featured.dart';
|
import 'package:island/widgets/post/post_featured.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:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:island/widgets/realm/realm_card.dart';
|
import 'package:island/widgets/realm/realm_card.dart';
|
||||||
import 'package:island/widgets/publisher/publisher_card.dart';
|
import 'package:island/widgets/publisher/publisher_card.dart';
|
||||||
@@ -101,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;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -137,10 +138,9 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
tooltip: 'explore'.tr(),
|
tooltip: 'explore'.tr(),
|
||||||
isSelected: currentFilter.value == null,
|
isSelected: currentFilter.value == null,
|
||||||
color:
|
color: currentFilter.value == null
|
||||||
currentFilter.value == null
|
? Theme.of(context).colorScheme.primary
|
||||||
? Theme.of(context).colorScheme.primary
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => handleFilterChange('subscriptions'),
|
onPressed: () => handleFilterChange('subscriptions'),
|
||||||
@@ -150,10 +150,9 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
tooltip: 'exploreFilterSubscriptions'.tr(),
|
tooltip: 'exploreFilterSubscriptions'.tr(),
|
||||||
isSelected: currentFilter.value == 'subscriptions',
|
isSelected: currentFilter.value == 'subscriptions',
|
||||||
color:
|
color: currentFilter.value == 'subscriptions'
|
||||||
currentFilter.value == 'subscriptions'
|
? Theme.of(context).colorScheme.primary
|
||||||
? Theme.of(context).colorScheme.primary
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => handleFilterChange('friends'),
|
onPressed: () => handleFilterChange('friends'),
|
||||||
@@ -163,10 +162,9 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
tooltip: 'exploreFilterFriends'.tr(),
|
tooltip: 'exploreFilterFriends'.tr(),
|
||||||
isSelected: currentFilter.value == 'friends',
|
isSelected: currentFilter.value == 'friends',
|
||||||
color:
|
color: currentFilter.value == 'friends'
|
||||||
currentFilter.value == 'friends'
|
? Theme.of(context).colorScheme.primary
|
||||||
? Theme.of(context).colorScheme.primary
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -179,57 +177,44 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
tooltip: 'webArticlesStand'.tr(),
|
tooltip: 'webArticlesStand'.tr(),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder:
|
itemBuilder: (context) => [
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.category),
|
||||||
const Icon(Symbols.category),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('categoriesAndTags').tr(),
|
||||||
Text('categories').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postCategories');
|
||||||
context.pushNamed('postCategories');
|
},
|
||||||
},
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.shuffle),
|
||||||
const Icon(Symbols.label),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('postShuffle').tr(),
|
||||||
Text('tags').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postShuffle');
|
||||||
context.pushNamed('postTags');
|
},
|
||||||
},
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.search),
|
||||||
const Icon(Symbols.shuffle),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('search').tr(),
|
||||||
Text('postShuffle').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postSearch');
|
||||||
context.pushNamed('postShuffle');
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.search),
|
|
||||||
const Gap(12),
|
|
||||||
Text('search').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pushNamed('postSearch');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
icon: Icon(Symbols.action_key),
|
icon: Icon(Symbols.action_key),
|
||||||
tooltip: 'search'.tr(),
|
tooltip: 'search'.tr(),
|
||||||
),
|
),
|
||||||
@@ -237,10 +222,9 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
).padding(horizontal: 8, vertical: 4),
|
).padding(horizontal: 8, vertical: 4),
|
||||||
);
|
);
|
||||||
|
|
||||||
final appBar =
|
final appBar = isWide
|
||||||
isWide
|
? null
|
||||||
? null
|
: _buildAppBar(currentFilter.value, handleFilterChange, context);
|
||||||
: _buildAppBar(currentFilter.value, handleFilterChange, context);
|
|
||||||
|
|
||||||
final dragging = useState(false);
|
final dragging = useState(false);
|
||||||
|
|
||||||
@@ -263,19 +247,18 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
body:
|
body: isWide
|
||||||
isWide
|
? _buildWideBody(
|
||||||
? _buildWideBody(
|
context,
|
||||||
context,
|
ref,
|
||||||
ref,
|
filterBar,
|
||||||
filterBar,
|
user,
|
||||||
user,
|
notificationCount,
|
||||||
notificationCount,
|
query,
|
||||||
query,
|
events,
|
||||||
events,
|
selectedDay,
|
||||||
selectedDay,
|
)
|
||||||
)
|
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
|
||||||
),
|
),
|
||||||
if (dragging.value)
|
if (dragging.value)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@@ -295,12 +278,11 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text(
|
Text(
|
||||||
'dropToShare'.tr(),
|
'dropToShare'.tr(),
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.headlineMedium
|
||||||
context,
|
?.copyWith(
|
||||||
).textTheme.headlineMedium?.copyWith(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -321,9 +303,9 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
// Sliver list cannot provide refresh handled by the pagination list
|
// Sliver list cannot provide refresh handled by the pagination list
|
||||||
isRefreshable: false,
|
isRefreshable: false,
|
||||||
isSliver: true,
|
isSliver: true,
|
||||||
contentBuilder:
|
footerSkeletonChild: const PostItemSkeleton(),
|
||||||
(data, footer) =>
|
contentBuilder: (data, footer) =>
|
||||||
_ActivityListView(data: data, isWide: isWide, footer: footer),
|
_ActivityListView(data: data, isWide: isWide, footer: footer),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,39 +375,38 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
else
|
else
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child:
|
child: Column(
|
||||||
Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
children: [
|
||||||
children: [
|
const Icon(Symbols.emoji_people_rounded, size: 40),
|
||||||
const Icon(Symbols.emoji_people_rounded, size: 40),
|
const Gap(8),
|
||||||
const Gap(8),
|
Text(
|
||||||
Text(
|
'Welcome to\nthe Solar Network',
|
||||||
'Welcome to\nthe Solar Network',
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
).bold(),
|
||||||
).bold(),
|
const Gap(2),
|
||||||
const Gap(2),
|
Text(
|
||||||
Text(
|
'Login to explore more!',
|
||||||
'Login to explore more!',
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
textAlign: TextAlign.center,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
),
|
const Gap(4),
|
||||||
const Gap(4),
|
TextButton.icon(
|
||||||
TextButton.icon(
|
onPressed: () {
|
||||||
onPressed: () {
|
showModalBottomSheet(
|
||||||
showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
useRootNavigator: true,
|
||||||
useRootNavigator: true,
|
isScrollControlled: true,
|
||||||
isScrollControlled: true,
|
builder: (context) => LoginModal(),
|
||||||
builder: (context) => LoginModal(),
|
);
|
||||||
);
|
},
|
||||||
},
|
icon: const Icon(Symbols.login),
|
||||||
icon: const Icon(Symbols.login),
|
label: Text('login').tr(),
|
||||||
label: Text('login').tr(),
|
),
|
||||||
),
|
],
|
||||||
],
|
).padding(horizontal: 36, vertical: 16).center(),
|
||||||
).padding(horizontal: 36, vertical: 16).center(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12);
|
).padding(horizontal: 12);
|
||||||
@@ -491,57 +472,44 @@ class ExploreScreen extends HookConsumerWidget {
|
|||||||
tooltip: 'webArticlesStand'.tr(),
|
tooltip: 'webArticlesStand'.tr(),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
itemBuilder:
|
itemBuilder: (context) => [
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.category),
|
||||||
const Icon(Symbols.category),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('categoriesAndTags').tr(),
|
||||||
Text('categories').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postCategories');
|
||||||
context.pushNamed('postCategories');
|
},
|
||||||
},
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.shuffle),
|
||||||
const Icon(Symbols.label),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('postShuffle').tr(),
|
||||||
Text('tags').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postShuffle');
|
||||||
context.pushNamed('postTags');
|
},
|
||||||
},
|
),
|
||||||
),
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.search),
|
||||||
const Icon(Symbols.shuffle),
|
const Gap(12),
|
||||||
const Gap(12),
|
Text('search').tr(),
|
||||||
Text('postShuffle').tr(),
|
],
|
||||||
],
|
),
|
||||||
),
|
onTap: () {
|
||||||
onTap: () {
|
context.pushNamed('postSearch');
|
||||||
context.pushNamed('postShuffle');
|
},
|
||||||
},
|
),
|
||||||
),
|
],
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.search),
|
|
||||||
const Gap(12),
|
|
||||||
Text('search').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.pushNamed('postSearch');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
icon: Icon(Symbols.action_key, color: foregroundColor),
|
icon: Icon(Symbols.action_key, color: foregroundColor),
|
||||||
tooltip: 'search'.tr(),
|
tooltip: 'search'.tr(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,17 +177,15 @@ 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: (context) => SheetScaffold(
|
||||||
builder:
|
titleText: 'File References',
|
||||||
(context) => SheetScaffold(
|
child: ReferencesList(fileId: item.id),
|
||||||
titleText: 'File References',
|
),
|
||||||
child: ReferencesList(fileId: item.id),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -300,43 +299,39 @@ 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];
|
return ListTile(
|
||||||
return ListTile(
|
leading: const Icon(Icons.link),
|
||||||
leading: const Icon(Icons.link),
|
title: Row(
|
||||||
title: Row(
|
spacing: 6,
|
||||||
spacing: 6,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
reference.usage,
|
||||||
reference.usage,
|
style: GoogleFonts.robotoMono(
|
||||||
style: GoogleFonts.robotoMono(
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 13,
|
||||||
fontSize: 13,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
reference.id,
|
|
||||||
style: GoogleFonts.robotoMono(fontSize: 13),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
subtitle: Row(
|
Text(reference.id, style: GoogleFonts.robotoMono(fontSize: 13)),
|
||||||
spacing: 8,
|
],
|
||||||
children: [
|
),
|
||||||
Text(reference.createdAt.formatRelative(context)),
|
subtitle: Row(
|
||||||
const VerticalDivider(width: 1, thickness: 1).height(12),
|
spacing: 8,
|
||||||
Text(reference.createdAt.formatSystem()),
|
children: [
|
||||||
],
|
Text(reference.createdAt.formatRelative(context)),
|
||||||
),
|
const VerticalDivider(width: 1, thickness: 1).height(12),
|
||||||
);
|
Text(reference.createdAt.formatSystem()),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
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')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,38 +40,31 @@ 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:
|
usage: usage,
|
||||||
(quota) => FileListView(
|
quota: quota,
|
||||||
usage: usage,
|
currentPath: currentPath,
|
||||||
quota: quota,
|
selectedPool: selectedPool,
|
||||||
currentPath: currentPath,
|
onPickAndUpload: () => _pickAndUploadFile(
|
||||||
selectedPool: selectedPool,
|
ref,
|
||||||
onPickAndUpload:
|
currentPath.value,
|
||||||
() => _pickAndUploadFile(
|
selectedPool.value?.id,
|
||||||
ref,
|
|
||||||
currentPath.value,
|
|
||||||
selectedPool.value?.id,
|
|
||||||
),
|
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
|
||||||
mode: mode,
|
|
||||||
viewMode: viewMode,
|
|
||||||
),
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
|
||||||
),
|
),
|
||||||
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
|
mode: mode,
|
||||||
|
viewMode: viewMode,
|
||||||
|
),
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||||
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('Error loading usage')),
|
error: (e, _) => Center(child: Text('Error loading usage')),
|
||||||
),
|
),
|
||||||
@@ -158,44 +151,43 @@ 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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
const Gap(8),
|
||||||
const Gap(8),
|
TextField(
|
||||||
TextField(
|
controller: controller,
|
||||||
controller: controller,
|
decoration: const InputDecoration(
|
||||||
decoration: const InputDecoration(
|
labelText: 'Directory path',
|
||||||
labelText: 'Directory path',
|
hintText: 'e.g., documents, projects/my-app',
|
||||||
hintText: 'e.g., documents, projects/my-app',
|
helperText:
|
||||||
helperText:
|
'Enter a directory path. The directory will be created when you upload files to it.',
|
||||||
'Enter a directory path. The directory will be created when you upload files to it.',
|
helperMaxLines: 3,
|
||||||
helperMaxLines: 3,
|
border: OutlineInputBorder(
|
||||||
border: OutlineInputBorder(
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSubmitted: (_) {
|
|
||||||
handleChangeDirectory(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
onSubmitted: (_) {
|
||||||
|
handleChangeDirectory(context);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
],
|
||||||
TextButton(
|
),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
actions: [
|
||||||
child: const Text('Cancel'),
|
TextButton(
|
||||||
),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
TextButton.icon(
|
child: const Text('Cancel'),
|
||||||
onPressed: () => handleChangeDirectory(context),
|
|
||||||
label: const Text('Go to Directory'),
|
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => handleChangeDirectory(context),
|
||||||
|
label: const Text('Go to Directory'),
|
||||||
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,14 +199,13 @@ 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,
|
quota: quota,
|
||||||
quota: quota,
|
).padding(horizontal: 8, vertical: 16),
|
||||||
).padding(horizontal: 8, vertical: 16),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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: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/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
@@ -10,7 +11,6 @@ import 'package:island/pods/network.dart';
|
|||||||
import 'package:island/pods/paging.dart';
|
import 'package:island/pods/paging.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
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/content/sheet.dart';
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,11 +182,10 @@ class NotificationListNotifier extends AsyncNotifier<List<SnNotification>>
|
|||||||
queryParameters: queryParams,
|
queryParameters: queryParams,
|
||||||
);
|
);
|
||||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||||
final notifications =
|
final notifications = response.data
|
||||||
response.data
|
.map((json) => SnNotification.fromJson(json))
|
||||||
.map((json) => SnNotification.fromJson(json))
|
.cast<SnNotification>()
|
||||||
.cast<SnNotification>()
|
.toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
final unreadCount = notifications.where((n) => n.viewedAt == null).length;
|
final unreadCount = notifications.where((n) => n.viewedAt == null).length;
|
||||||
ref.read(notificationUnreadCountProvider.notifier).decrement(unreadCount);
|
ref.read(notificationUnreadCountProvider.notifier).decrement(unreadCount);
|
||||||
@@ -145,15 +225,22 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Refresh unread count when sheet opens to sync across devices
|
// Refresh unread count when sheet opens to sync across devices
|
||||||
ref.read(notificationUnreadCountProvider.notifier).refresh();
|
useEffect(() {
|
||||||
|
Future(() {
|
||||||
|
ref.read(notificationUnreadCountProvider.notifier).refresh();
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
final isLoading = useState(false);
|
||||||
|
|
||||||
Future<void> markAllRead() async {
|
Future<void> markAllRead() async {
|
||||||
showLoadingModal(context);
|
isLoading.value = true;
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
await apiClient.post('/ring/notifications/all/read');
|
await apiClient.post('/ring/notifications/all/read');
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
hideLoadingModal(context);
|
isLoading.value = false;
|
||||||
ref.invalidate(notificationListProvider);
|
ref.read(notificationListProvider.notifier).refresh();
|
||||||
ref.watch(notificationUnreadCountProvider.notifier).clear();
|
ref.watch(notificationUnreadCountProvider.notifier).clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,108 +252,126 @@ class NotificationSheet extends HookConsumerWidget {
|
|||||||
icon: const Icon(Symbols.mark_as_unread),
|
icon: const Icon(Symbols.mark_as_unread),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: PaginationList(
|
child: Column(
|
||||||
provider: notificationListProvider,
|
children: [
|
||||||
notifier: notificationListProvider.notifier,
|
if (isLoading.value)
|
||||||
itemBuilder: (context, index, notification) {
|
LinearProgressIndicator(
|
||||||
final pfp = notification.meta['pfp'] as String?;
|
minHeight: 2,
|
||||||
final images = notification.meta['images'] as List?;
|
color: Theme.of(context).colorScheme.primary,
|
||||||
final imageIds = images?.cast<String>() ?? [];
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
isThreeLine: true,
|
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
leading:
|
|
||||||
pfp != null
|
|
||||||
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child: Icon(
|
|
||||||
_getNotificationIcon(notification.topic),
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(notification.title),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
if (notification.subtitle.isNotEmpty)
|
|
||||||
Text(notification.subtitle).bold(),
|
|
||||||
Row(
|
|
||||||
spacing: 6,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
DateFormat().format(notification.createdAt.toLocal()),
|
|
||||||
).fontSize(11),
|
|
||||||
Text('·').fontSize(11).bold(),
|
|
||||||
Text(
|
|
||||||
RelativeTime(
|
|
||||||
context,
|
|
||||||
).format(notification.createdAt.toLocal()),
|
|
||||||
).fontSize(11),
|
|
||||||
],
|
|
||||||
).opacity(0.75).padding(bottom: 4),
|
|
||||||
MarkdownTextContent(
|
|
||||||
content: notification.content,
|
|
||||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (imageIds.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children:
|
|
||||||
imageIds.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:
|
Expanded(
|
||||||
notification.viewedAt != null
|
child: PaginationList(
|
||||||
? null
|
provider: notificationListProvider,
|
||||||
: Container(
|
notifier: notificationListProvider.notifier,
|
||||||
width: 12,
|
footerSkeletonChild: const SkeletonNotificationTile(),
|
||||||
height: 12,
|
itemBuilder: (context, index, notification) {
|
||||||
decoration: const BoxDecoration(
|
final pfp = notification.meta['pfp'] as String?;
|
||||||
color: Colors.blue,
|
final images = notification.meta['images'] as List?;
|
||||||
shape: BoxShape.circle,
|
final imageIds = images?.cast<String>() ?? [];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
isThreeLine: true,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
leading: pfp != null
|
||||||
|
? ProfilePictureWidget(fileId: pfp, radius: 20)
|
||||||
|
: CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primaryContainer,
|
||||||
|
child: Icon(
|
||||||
|
_getNotificationIcon(notification.topic),
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(notification.title),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (notification.subtitle.isNotEmpty)
|
||||||
|
Text(notification.subtitle).bold(),
|
||||||
|
Row(
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat().format(
|
||||||
|
notification.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
).fontSize(11),
|
||||||
|
Text('·').fontSize(11).bold(),
|
||||||
|
Text(
|
||||||
|
RelativeTime(
|
||||||
|
context,
|
||||||
|
).format(notification.createdAt.toLocal()),
|
||||||
|
).fontSize(11),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(bottom: 4),
|
||||||
|
MarkdownTextContent(
|
||||||
|
content: notification.content,
|
||||||
|
textStyle: Theme.of(context).textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withOpacity(0.8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (imageIds.isNotEmpty)
|
||||||
onTap: () {
|
Padding(
|
||||||
if (notification.meta['action_uri'] != null) {
|
padding: const EdgeInsets.only(top: 8),
|
||||||
var uri = notification.meta['action_uri'] as String;
|
child: Wrap(
|
||||||
if (uri.startsWith('/')) {
|
spacing: 8,
|
||||||
// In-app routes
|
runSpacing: 8,
|
||||||
rootNavigatorKey.currentContext?.push(
|
children: imageIds.map((imageId) {
|
||||||
notification.meta['action_uri'],
|
return SizedBox(
|
||||||
);
|
width: 80,
|
||||||
} else {
|
height: 80,
|
||||||
// External URLs
|
child: ClipRRect(
|
||||||
launchUrlString(uri);
|
borderRadius: BorderRadius.circular(8),
|
||||||
}
|
child: CloudImageWidget(
|
||||||
}
|
fileId: imageId,
|
||||||
},
|
aspectRatio: 1,
|
||||||
);
|
fit: BoxFit.cover,
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: notification.viewedAt != null
|
||||||
|
? null
|
||||||
|
: Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (notification.meta['action_uri'] != null) {
|
||||||
|
var uri = notification.meta['action_uri'] as String;
|
||||||
|
if (uri.startsWith('/')) {
|
||||||
|
// In-app routes
|
||||||
|
rootNavigatorKey.currentContext?.push(
|
||||||
|
notification.meta['action_uri'],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// External URLs
|
||||||
|
launchUrlString(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ _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: json['forwarding_to'] == null
|
||||||
forwardingTo:
|
? null
|
||||||
json['forwarding_to'] == null
|
: SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>),
|
||||||
? null
|
|
||||||
: SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$PostComposeInitialStateToJson(
|
Map<String, dynamic> _$PostComposeInitialStateToJson(
|
||||||
|
|||||||
@@ -2,118 +2,113 @@ 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(
|
||||||
provider: postCategoriesProvider,
|
isNoBackground: false,
|
||||||
notifier: postCategoriesProvider.notifier,
|
appBar: AppBar(
|
||||||
padding: EdgeInsets.zero,
|
title: const Text('categoriesAndTags').tr(),
|
||||||
itemBuilder: (context, index, category) {
|
bottom: TabBar(
|
||||||
return ListTile(
|
tabs: [
|
||||||
leading: const Icon(Symbols.category),
|
Tab(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
child: Text(
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
'categories'.tr(),
|
||||||
title: Text(category.categoryDisplayTitle),
|
style: TextStyle(
|
||||||
subtitle: Text('postCount'.plural(category.usage)),
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
onTap: () {
|
),
|
||||||
context.pushNamed(
|
),
|
||||||
'postCategoryDetail',
|
),
|
||||||
pathParameters: {'slug': category.slug},
|
Tab(
|
||||||
);
|
child: Text(
|
||||||
},
|
'tags'.tr(),
|
||||||
);
|
style: TextStyle(
|
||||||
},
|
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: const TabBarView(children: [_CategoriesTab(), _TagsTab()]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostTagsListScreen extends ConsumerWidget {
|
class _CategoriesTab extends ConsumerWidget {
|
||||||
const PostTagsListScreen({super.key});
|
const _CategoriesTab();
|
||||||
|
|
||||||
@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()),
|
provider: postCategoriesProvider,
|
||||||
body: PaginationList(
|
notifier: postCategoriesProvider.notifier,
|
||||||
provider: postTagsProvider,
|
footerSkeletonMaxWidth: 640,
|
||||||
notifier: postTagsProvider.notifier,
|
padding: EdgeInsets.zero,
|
||||||
padding: EdgeInsets.zero,
|
itemBuilder: (context, index, category) {
|
||||||
itemBuilder: (context, index, tag) {
|
return Center(
|
||||||
return ListTile(
|
child: ConstrainedBox(
|
||||||
title: Text(tag.name ?? '#${tag.slug}'),
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
child: ListTile(
|
||||||
leading: const Icon(Symbols.label),
|
leading: const Icon(Symbols.category),
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
subtitle: Text('postCount'.plural(tag.usage)),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
title: Text(category.categoryDisplayTitle),
|
||||||
context.pushNamed(
|
subtitle: Text('postCount'.plural(category.usage)),
|
||||||
'postTagDetail',
|
onTap: () {
|
||||||
pathParameters: {'slug': tag.slug},
|
context.pushNamed(
|
||||||
);
|
'postCategoryDetail',
|
||||||
},
|
pathParameters: {'slug': category.slug},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TagsTab extends ConsumerWidget {
|
||||||
|
const _TagsTab();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return PaginationList(
|
||||||
|
provider: postTagsProvider,
|
||||||
|
notifier: postTagsProvider.notifier,
|
||||||
|
footerSkeletonMaxWidth: 640,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemBuilder: (context, index, tag) {
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(tag.name ?? '#${tag.slug}'),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: const Icon(Symbols.label),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
subtitle: Text('postCount'.plural(tag.usage)),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'postTagDetail',
|
||||||
|
pathParameters: {'slug': tag.slug},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +83,17 @@ 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';
|
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
@@ -108,63 +109,50 @@ 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(
|
category.categoryDisplayTitle,
|
||||||
category.categoryDisplayTitle,
|
).bold().fontSize(15),
|
||||||
).bold().fontSize(15),
|
Text('A category'),
|
||||||
Text('A category'),
|
const Gap(8),
|
||||||
const Gap(8),
|
subscriptionStatus.when(
|
||||||
subscriptionStatus.when(
|
data: (isSubscribed) => isSubscribed
|
||||||
data:
|
? FilledButton.icon(
|
||||||
(isSubscribed) =>
|
onPressed: () async {
|
||||||
isSubscribed
|
await _unsubscribeFromCategoryOrTag(
|
||||||
? FilledButton.icon(
|
ref,
|
||||||
onPressed: () async {
|
slug: slug,
|
||||||
await _unsubscribeFromCategoryOrTag(
|
isCategory: isCategory,
|
||||||
ref,
|
);
|
||||||
slug: slug,
|
},
|
||||||
isCategory: isCategory,
|
icon: const Icon(Symbols.remove_circle),
|
||||||
);
|
label: Text('unsubscribe'.tr()),
|
||||||
},
|
)
|
||||||
icon: const Icon(
|
: FilledButton.icon(
|
||||||
Symbols.remove_circle,
|
onPressed: () async {
|
||||||
),
|
await _subscribeToCategoryOrTag(
|
||||||
label: Text('unsubscribe'.tr()),
|
ref,
|
||||||
)
|
slug: slug,
|
||||||
: FilledButton.icon(
|
isCategory: isCategory,
|
||||||
onPressed: () async {
|
);
|
||||||
await _subscribeToCategoryOrTag(
|
},
|
||||||
ref,
|
icon: const Icon(Symbols.add_circle),
|
||||||
slug: slug,
|
label: Text('subscribe'.tr()),
|
||||||
isCategory: isCategory,
|
),
|
||||||
);
|
error: (error, _) =>
|
||||||
},
|
Text('Error loading subscription status'),
|
||||||
icon: const Icon(
|
loading: () =>
|
||||||
Symbols.add_circle,
|
CircularProgressIndicator().center(),
|
||||||
),
|
|
||||||
label: Text('subscribe'.tr()),
|
|
||||||
),
|
|
||||||
error:
|
|
||||||
(error, _) => Text(
|
|
||||||
'Error loading subscription status',
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() =>
|
|
||||||
CircularProgressIndicator().center(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, vertical: 16),
|
|
||||||
error:
|
|
||||||
(error, _) => ResponseErrorWidget(
|
|
||||||
error: error,
|
|
||||||
onRetry:
|
|
||||||
() => ref.invalidate(
|
|
||||||
postCategoryProvider(slug),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 16),
|
||||||
|
error: (error, _) => ResponseErrorWidget(
|
||||||
|
error: error,
|
||||||
|
onRetry: () =>
|
||||||
|
ref.invalidate(postCategoryProvider(slug)),
|
||||||
|
),
|
||||||
loading: () => ResponseLoadingWidget(),
|
loading: () => ResponseLoadingWidget(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -179,61 +167,49 @@ 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(
|
tag.name ?? '#${tag.slug}',
|
||||||
tag.name ?? '#${tag.slug}',
|
).bold().fontSize(15),
|
||||||
).bold().fontSize(15),
|
Text('A tag'),
|
||||||
Text('A tag'),
|
const Gap(8),
|
||||||
const Gap(8),
|
subscriptionStatus.when(
|
||||||
subscriptionStatus.when(
|
data: (isSubscribed) => isSubscribed
|
||||||
data:
|
? FilledButton.icon(
|
||||||
(isSubscribed) =>
|
onPressed: () async {
|
||||||
isSubscribed
|
await _unsubscribeFromCategoryOrTag(
|
||||||
? FilledButton.icon(
|
ref,
|
||||||
onPressed: () async {
|
slug: slug,
|
||||||
await _unsubscribeFromCategoryOrTag(
|
isCategory: isCategory,
|
||||||
ref,
|
);
|
||||||
slug: slug,
|
},
|
||||||
isCategory: isCategory,
|
icon: const Icon(Symbols.remove_circle),
|
||||||
);
|
label: Text('unsubscribe'.tr()),
|
||||||
},
|
)
|
||||||
icon: const Icon(
|
: FilledButton.icon(
|
||||||
Symbols.remove_circle,
|
onPressed: () async {
|
||||||
),
|
await _subscribeToCategoryOrTag(
|
||||||
label: Text('unsubscribe'.tr()),
|
ref,
|
||||||
)
|
slug: slug,
|
||||||
: FilledButton.icon(
|
isCategory: isCategory,
|
||||||
onPressed: () async {
|
);
|
||||||
await _subscribeToCategoryOrTag(
|
},
|
||||||
ref,
|
icon: const Icon(Symbols.add_circle),
|
||||||
slug: slug,
|
label: Text('subscribe'.tr()),
|
||||||
isCategory: isCategory,
|
),
|
||||||
);
|
error: (error, _) =>
|
||||||
},
|
Text('Error loading subscription status'),
|
||||||
icon: const Icon(
|
loading: () =>
|
||||||
Symbols.add_circle,
|
CircularProgressIndicator().center(),
|
||||||
),
|
|
||||||
label: Text('subscribe'.tr()),
|
|
||||||
),
|
|
||||||
error:
|
|
||||||
(error, _) => Text(
|
|
||||||
'Error loading subscription status',
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() =>
|
|
||||||
CircularProgressIndicator().center(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 24, vertical: 16),
|
|
||||||
error:
|
|
||||||
(error, _) => ResponseErrorWidget(
|
|
||||||
error: error,
|
|
||||||
onRetry:
|
|
||||||
() => ref.invalidate(postTagProvider(slug)),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 24, vertical: 16),
|
||||||
|
error: (error, _) => ResponseErrorWidget(
|
||||||
|
error: error,
|
||||||
|
onRetry: () => ref.invalidate(postTagProvider(slug)),
|
||||||
|
),
|
||||||
loading: () => ResponseLoadingWidget(),
|
loading: () => ResponseLoadingWidget(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -242,8 +218,11 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SliverGap(4),
|
const SliverGap(4),
|
||||||
SliverPostList(
|
SliverPostList(
|
||||||
categories: isCategory ? [slug] : null,
|
query: PostListQuery(
|
||||||
tags: isCategory ? null : [slug],
|
categories: isCategory ? [slug] : null,
|
||||||
|
tags: isCategory ? null : [slug],
|
||||||
|
),
|
||||||
|
|
||||||
maxWidth: 540 + 16,
|
maxWidth: 540 + 16,
|
||||||
),
|
),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 8),
|
SliverGap(MediaQuery.of(context).padding.bottom + 8),
|
||||||
|
|||||||
@@ -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,225 +48,256 @@ 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(
|
hideSearch: true,
|
||||||
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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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)
|
||||||
slivers: [
|
? Row(
|
||||||
if (showFilters.value)
|
children: [
|
||||||
SliverToBoxAdapter(
|
Flexible(
|
||||||
child: Center(
|
flex: 4,
|
||||||
child: ConstrainedBox(
|
child: ExtendedRefreshIndicator(
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
onRefresh: noti.refresh,
|
||||||
child: buildFilterPanel(),
|
child: CustomScrollView(
|
||||||
),
|
slivers: [
|
||||||
),
|
SliverGap(16),
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
// Use PaginationList with isSliver=true
|
child: Padding(
|
||||||
PaginationList(
|
padding: const EdgeInsets.symmetric(
|
||||||
provider: postSearchProvider,
|
horizontal: 8,
|
||||||
notifier: postSearchProvider.notifier,
|
),
|
||||||
isSliver: true,
|
child: SearchBar(
|
||||||
isRefreshable:
|
elevation: WidgetStateProperty.all(4),
|
||||||
false, // CustomScrollView handles refreshing usually, but here we don't have PullToRefresh
|
controller: searchController,
|
||||||
itemBuilder: (context, index, post) {
|
hintText: 'search'.tr(),
|
||||||
return Center(
|
leading: const Icon(Icons.search),
|
||||||
child: ConstrainedBox(
|
padding: WidgetStateProperty.all(
|
||||||
constraints: BoxConstraints(maxWidth: 600),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Card(
|
),
|
||||||
margin: EdgeInsets.symmetric(
|
onChanged: onSearchChanged,
|
||||||
horizontal: 8,
|
onSubmitted: (value) {
|
||||||
vertical: 4,
|
onSearchChanged(value, skipDebounce: true);
|
||||||
),
|
},
|
||||||
child: PostActionableItem(item: post, borderRadius: 8),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(
|
||||||
if (searchState.value?.isEmpty == true &&
|
alignment: Alignment.topLeft,
|
||||||
searchController.text.isNotEmpty &&
|
child: SingleChildScrollView(
|
||||||
!searchState.isLoading)
|
child: Column(
|
||||||
SliverFillRemaining(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
child: Center(child: Text('noResultsFound'.tr())),
|
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)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
|
child: buildFilterPanel(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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 Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 600),
|
||||||
|
child: 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())),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,45 +205,41 @@ class _PublisherBasisWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
subStatus
|
subStatus
|
||||||
.when(
|
.when(
|
||||||
data:
|
data: (status) => FilledButton.icon(
|
||||||
(status) => FilledButton.icon(
|
onPressed: subscribing.value
|
||||||
onPressed:
|
? null
|
||||||
subscribing.value
|
: (status.isSubscribed
|
||||||
? null
|
? unsubscribe
|
||||||
: (status.isSubscribed
|
: subscribe),
|
||||||
? unsubscribe
|
icon: Icon(
|
||||||
: subscribe),
|
status.isSubscribed
|
||||||
icon: Icon(
|
? Symbols.remove_circle
|
||||||
status.isSubscribed
|
: Symbols.add_circle,
|
||||||
? Symbols.remove_circle
|
),
|
||||||
: Symbols.add_circle,
|
label: Text(
|
||||||
),
|
status.isSubscribed
|
||||||
label:
|
? 'unsubscribe'
|
||||||
Text(
|
: 'subscribe',
|
||||||
status.isSubscribed
|
).tr(),
|
||||||
? 'unsubscribe'
|
style: ButtonStyle(
|
||||||
: 'subscribe',
|
visualDensity: VisualDensity(
|
||||||
).tr(),
|
vertical: -2,
|
||||||
style: ButtonStyle(
|
|
||||||
visualDensity: VisualDensity(
|
|
||||||
vertical: -2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
error: (_, _) => const SizedBox(),
|
error: (_, _) => const SizedBox(),
|
||||||
loading:
|
loading: () => const SizedBox(
|
||||||
() => const SizedBox(
|
height: 36,
|
||||||
height: 36,
|
child: Center(
|
||||||
child: Center(
|
child: SizedBox(
|
||||||
child: SizedBox(
|
width: 20,
|
||||||
width: 20,
|
height: 20,
|
||||||
height: 20,
|
child: CircularProgressIndicator(
|
||||||
child: CircularProgressIndicator(
|
strokeWidth: 2,
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.padding(vertical: 12),
|
.padding(vertical: 12),
|
||||||
],
|
],
|
||||||
@@ -271,10 +271,10 @@ class _PublisherBadgesWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return (badges.value?.isNotEmpty ?? false)
|
return (badges.value?.isNotEmpty ?? false)
|
||||||
? Card(
|
? Card(
|
||||||
child: BadgeList(
|
child: BadgeList(
|
||||||
badges: badges.value!,
|
badges: badges.value!,
|
||||||
).padding(horizontal: 26, vertical: 20),
|
).padding(horizontal: 26, vertical: 20),
|
||||||
).padding(horizontal: 4)
|
).padding(horizontal: 4)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,9 +288,9 @@ class _PublisherVerificationWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return (data.verification != null)
|
return (data.verification != null)
|
||||||
? Card(
|
? Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: VerificationStatusCard(mark: data.verification!),
|
child: VerificationStatusCard(mark: data.verification!),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,285 +333,18 @@ 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) =>
|
? ActivityHeatmapWidget(
|
||||||
data != null
|
heatmap: data,
|
||||||
? ActivityHeatmapWidget(
|
forceDense: forceDense,
|
||||||
heatmap: data,
|
).padding(horizontal: 8)
|
||||||
forceDense: forceDense,
|
: const SizedBox.shrink(),
|
||||||
).padding(horizontal: 8)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,205 +470,34 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return publisher.when(
|
return publisher.when(
|
||||||
data:
|
data: (data) => AppScaffold(
|
||||||
(data) => AppScaffold(
|
isNoBackground: false,
|
||||||
isNoBackground: false,
|
appBar: isWideScreen(context)
|
||||||
appBar:
|
? AppBar(
|
||||||
isWideScreen(context)
|
foregroundColor: appbarColor.value,
|
||||||
? AppBar(
|
leading: PageBackButton(
|
||||||
foregroundColor: appbarColor.value,
|
color: appbarColor.value,
|
||||||
leading: PageBackButton(
|
shadows: [appbarShadow],
|
||||||
color: appbarColor.value,
|
),
|
||||||
shadows: [appbarShadow],
|
title: Text(
|
||||||
),
|
data.nick,
|
||||||
title: Text(
|
style: TextStyle(
|
||||||
data.nick,
|
color:
|
||||||
style: TextStyle(
|
appbarColor.value ??
|
||||||
color:
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
appbarColor.value ??
|
shadows: [appbarShadow],
|
||||||
Theme.of(context).appBarTheme.foregroundColor,
|
),
|
||||||
shadows: [appbarShadow],
|
),
|
||||||
),
|
)
|
||||||
),
|
: null,
|
||||||
)
|
body: isWideScreen(context)
|
||||||
: null,
|
? Row(
|
||||||
body:
|
children: [
|
||||||
isWideScreen(context)
|
Flexible(
|
||||||
? Row(
|
flex: 4,
|
||||||
children: [
|
child: CustomScrollView(
|
||||||
Flexible(
|
|
||||||
flex: 4,
|
|
||||||
child: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverGap(16),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
child: ListTile(
|
|
||||||
title: Text('pinnedPosts'.tr()),
|
|
||||||
leading: const Icon(Symbols.push_pin),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Icon(
|
|
||||||
isPinnedExpanded.value
|
|
||||||
? Symbols.expand_less
|
|
||||||
: Symbols.expand_more,
|
|
||||||
),
|
|
||||||
onTap:
|
|
||||||
() =>
|
|
||||||
isPinnedExpanded.value =
|
|
||||||
!isPinnedExpanded.value,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
...[
|
|
||||||
if (isPinnedExpanded.value)
|
|
||||||
SliverPostList(pubName: name, pinned: true),
|
|
||||||
],
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherCategoryTabWidget(
|
|
||||||
categoryTabController: categoryTabController,
|
|
||||||
includeReplies: includeReplies,
|
|
||||||
mediaOnly: mediaOnly,
|
|
||||||
queryTerm: queryTerm,
|
|
||||||
order: order,
|
|
||||||
orderDesc: orderDesc,
|
|
||||||
periodStart: periodStart,
|
|
||||||
periodEnd: periodEnd,
|
|
||||||
showAdvancedFilters: showAdvancedFilters,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverPostList(
|
|
||||||
key: ValueKey(
|
|
||||||
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(left: 8),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 3,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_PublisherBasisWidget(
|
|
||||||
data: data,
|
|
||||||
subStatus: subStatus,
|
|
||||||
subscribing: subscribing,
|
|
||||||
subscribe: subscribe,
|
|
||||||
unsubscribe: unsubscribe,
|
|
||||||
).padding(horizontal: 4, top: 20),
|
|
||||||
_PublisherBadgesWidget(
|
|
||||||
data: data,
|
|
||||||
badges: badges,
|
|
||||||
),
|
|
||||||
_PublisherVerificationWidget(data: data),
|
|
||||||
_PublisherBioWidget(data: data),
|
|
||||||
_PublisherHeatmapWidget(
|
|
||||||
heatmap: heatmap,
|
|
||||||
forceDense: true,
|
|
||||||
).padding(vertical: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverGap(16),
|
||||||
foregroundColor: appbarColor.value,
|
|
||||||
expandedHeight: 180,
|
|
||||||
pinned: true,
|
|
||||||
leading: PageBackButton(
|
|
||||||
color: appbarColor.value,
|
|
||||||
shadows: [appbarShadow],
|
|
||||||
),
|
|
||||||
flexibleSpace: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child:
|
|
||||||
data.background?.id != null
|
|
||||||
? CloudImageWidget(
|
|
||||||
file: data.background,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.backgroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlexibleSpaceBar(
|
|
||||||
title: Text(
|
|
||||||
data.nick,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
appbarColor.value ??
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.foregroundColor,
|
|
||||||
shadows: [appbarShadow],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
background:
|
|
||||||
Container(), // Empty container since background is handled by Stack
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherBasisWidget(
|
|
||||||
data: data,
|
|
||||||
subStatus: subStatus,
|
|
||||||
subscribing: subscribing,
|
|
||||||
subscribe: subscribe,
|
|
||||||
unsubscribe: unsubscribe,
|
|
||||||
).padding(horizontal: 4, top: 8),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherBadgesWidget(
|
|
||||||
data: data,
|
|
||||||
badges: badges,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherVerificationWidget(data: data),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherBioWidget(data: data),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: _PublisherHeatmapWidget(
|
|
||||||
heatmap: heatmap,
|
|
||||||
).padding(vertical: 4),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
@@ -946,69 +506,180 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text('pinnedPosts'.tr()),
|
title: Text('pinnedPosts'.tr()),
|
||||||
|
leading: const Icon(Symbols.push_pin),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
isPinnedExpanded.value
|
isPinnedExpanded.value
|
||||||
? 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),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 3,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_PublisherBasisWidget(
|
||||||
|
data: data,
|
||||||
|
subStatus: subStatus,
|
||||||
|
subscribing: subscribing,
|
||||||
|
subscribe: subscribe,
|
||||||
|
unsubscribe: unsubscribe,
|
||||||
|
).padding(horizontal: 4, top: 20),
|
||||||
|
_PublisherBadgesWidget(data: data, badges: badges),
|
||||||
|
_PublisherVerificationWidget(data: data),
|
||||||
|
_PublisherBioWidget(data: data),
|
||||||
|
_PublisherHeatmapWidget(
|
||||||
|
heatmap: heatmap,
|
||||||
|
forceDense: true,
|
||||||
|
).padding(vertical: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error:
|
],
|
||||||
(error, stackTrace) => AppScaffold(
|
)
|
||||||
isNoBackground: false,
|
: CustomScrollView(
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
slivers: [
|
||||||
body: Center(child: Text(error.toString())),
|
SliverAppBar(
|
||||||
),
|
foregroundColor: appbarColor.value,
|
||||||
loading:
|
expandedHeight: 180,
|
||||||
() => AppScaffold(
|
pinned: true,
|
||||||
isNoBackground: false,
|
leading: PageBackButton(
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
color: appbarColor.value,
|
||||||
body: Center(child: CircularProgressIndicator()),
|
shadows: [appbarShadow],
|
||||||
),
|
),
|
||||||
|
flexibleSpace: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: data.background?.id != null
|
||||||
|
? CloudImageWidget(file: data.background)
|
||||||
|
: Container(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).appBarTheme.backgroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FlexibleSpaceBar(
|
||||||
|
title: Text(
|
||||||
|
data.nick,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
appbarColor.value ??
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
shadows: [appbarShadow],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
background:
|
||||||
|
Container(), // Empty container since background is handled by Stack
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _PublisherBasisWidget(
|
||||||
|
data: data,
|
||||||
|
subStatus: subStatus,
|
||||||
|
subscribing: subscribing,
|
||||||
|
subscribe: subscribe,
|
||||||
|
unsubscribe: unsubscribe,
|
||||||
|
).padding(horizontal: 4, top: 8),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _PublisherBadgesWidget(data: data, badges: badges),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _PublisherVerificationWidget(data: data),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: _PublisherBioWidget(data: data)),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: _PublisherHeatmapWidget(
|
||||||
|
heatmap: heatmap,
|
||||||
|
).padding(vertical: 4),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('pinnedPosts'.tr()),
|
||||||
|
trailing: Icon(
|
||||||
|
isPinnedExpanded.value
|
||||||
|
? Symbols.expand_less
|
||||||
|
: Symbols.expand_more,
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
isPinnedExpanded.value = !isPinnedExpanded.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
if (isPinnedExpanded.value)
|
||||||
|
SliverPostList(
|
||||||
|
query: PostListQuery(pubName: name, pinned: true),
|
||||||
|
queryKey: 'publisher-$name-pinned',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: PostFilterWidget(
|
||||||
|
categoryTabController: categoryTabController,
|
||||||
|
initialQuery: queryState.value,
|
||||||
|
onQueryChanged: (newQuery) => queryState.value = newQuery,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverPostList(
|
||||||
|
key: ValueKey(queryState.value),
|
||||||
|
query: queryState.value,
|
||||||
|
queryKey: 'publisher-$name',
|
||||||
|
),
|
||||||
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(leading: const PageBackButton()),
|
||||||
|
body: Center(child: Text(error.toString())),
|
||||||
|
),
|
||||||
|
loading: () => AppScaffold(
|
||||||
|
isNoBackground: false,
|
||||||
|
appBar: AppBar(leading: const PageBackButton()),
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,204 +172,180 @@ class RealmDetailScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar:
|
appBar: isWideScreen(context)
|
||||||
isWideScreen(context)
|
? realmState.when(
|
||||||
? realmState.when(
|
data: (realm) => AppBar(
|
||||||
data:
|
foregroundColor: appbarColor.value,
|
||||||
(realm) => AppBar(
|
leading: PageBackButton(
|
||||||
foregroundColor: appbarColor.value,
|
color: appbarColor.value,
|
||||||
leading: PageBackButton(
|
shadows: [iconShadow],
|
||||||
color: appbarColor.value,
|
),
|
||||||
shadows: [iconShadow],
|
flexibleSpace: Stack(
|
||||||
),
|
children: [
|
||||||
flexibleSpace: Stack(
|
Positioned.fill(
|
||||||
children: [
|
child: realm!.background?.id != null
|
||||||
Positioned.fill(
|
? CloudImageWidget(fileId: realm.background!.id)
|
||||||
child:
|
: Container(
|
||||||
realm!.background?.id != null
|
color: Theme.of(
|
||||||
? CloudImageWidget(
|
context,
|
||||||
fileId: realm.background!.id,
|
).appBarTheme.backgroundColor,
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.backgroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlexibleSpaceBar(
|
|
||||||
title: Text(
|
|
||||||
realm.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
appbarColor.value ??
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.foregroundColor,
|
|
||||||
shadows: [iconShadow],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
background: Container(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.people, shadows: [iconShadow]),
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) =>
|
|
||||||
_RealmMemberListSheet(realmSlug: slug),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_RealmActionMenu(
|
|
||||||
realmSlug: slug,
|
|
||||||
iconShadow: iconShadow,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
error: (_, _) => AppBar(leading: PageBackButton()),
|
FlexibleSpaceBar(
|
||||||
loading: () => AppBar(leading: PageBackButton()),
|
title: Text(
|
||||||
)
|
realm.name,
|
||||||
: null,
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
appbarColor.value ??
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
shadows: [iconShadow],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
background: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.people, shadows: [iconShadow]),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
_RealmMemberListSheet(realmSlug: slug),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
error: (_, _) => AppBar(leading: PageBackButton()),
|
||||||
|
loading: () => AppBar(leading: PageBackButton()),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
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) =>
|
? Row(
|
||||||
isWideScreen(context)
|
children: [
|
||||||
? Row(
|
Flexible(
|
||||||
children: [
|
flex: 3,
|
||||||
Flexible(
|
child: CustomScrollView(
|
||||||
flex: 3,
|
|
||||||
child: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverPostList(realm: slug, pinned: true),
|
|
||||||
SliverPostList(realm: slug, pinned: false),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
flex: 2,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
realmIdentity.when(
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
data:
|
|
||||||
(identity) => Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
realmDescriptionWidget(realm!),
|
|
||||||
if (identity == null &&
|
|
||||||
realm.isCommunity)
|
|
||||||
realmActionWidget(realm)
|
|
||||||
else
|
|
||||||
const SizedBox.shrink(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
realmChatRoomListWidget(realm!),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8, top: 8)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverPostList(
|
||||||
expandedHeight: 180,
|
query: PostListQuery(realm: slug, pinned: true),
|
||||||
pinned: true,
|
|
||||||
foregroundColor: appbarColor.value,
|
|
||||||
leading: PageBackButton(
|
|
||||||
color: appbarColor.value,
|
|
||||||
shadows: [iconShadow],
|
|
||||||
),
|
|
||||||
flexibleSpace: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child:
|
|
||||||
realm!.background?.id != null
|
|
||||||
? CloudImageWidget(
|
|
||||||
fileId: realm.background!.id,
|
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.backgroundColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FlexibleSpaceBar(
|
|
||||||
title: Text(
|
|
||||||
realm.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
appbarColor.value ??
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).appBarTheme.foregroundColor,
|
|
||||||
shadows: [iconShadow],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
background:
|
|
||||||
Container(), // Empty container since background is handled by Stack
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.people, shadows: [iconShadow]),
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => _RealmMemberListSheet(
|
|
||||||
realmSlug: slug,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_RealmActionMenu(
|
|
||||||
realmSlug: slug,
|
|
||||||
iconShadow: iconShadow,
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
SliverGap(4),
|
SliverPostList(
|
||||||
SliverToBoxAdapter(
|
query: PostListQuery(realm: slug, pinned: false),
|
||||||
child: realmIdentity.when(
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
data:
|
|
||||||
(identity) => Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
realmDescriptionWidget(realm),
|
|
||||||
if (identity == null && realm.isCommunity)
|
|
||||||
realmActionWidget(realm)
|
|
||||||
else
|
|
||||||
const SizedBox.shrink(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: realmChatRoomListWidget(realm),
|
|
||||||
),
|
|
||||||
SliverPostList(realm: slug, pinned: true),
|
|
||||||
SliverPostList(realm: slug, pinned: false),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
realmIdentity.when(
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
data: (identity) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
realmDescriptionWidget(realm!),
|
||||||
|
if (identity == null && realm.isCommunity)
|
||||||
|
realmActionWidget(realm)
|
||||||
|
else
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
realmChatRoomListWidget(realm!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 8, top: 8)
|
||||||
|
: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
expandedHeight: 180,
|
||||||
|
pinned: true,
|
||||||
|
foregroundColor: appbarColor.value,
|
||||||
|
leading: PageBackButton(
|
||||||
|
color: appbarColor.value,
|
||||||
|
shadows: [iconShadow],
|
||||||
|
),
|
||||||
|
flexibleSpace: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: realm!.background?.id != null
|
||||||
|
? CloudImageWidget(fileId: realm.background!.id)
|
||||||
|
: Container(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).appBarTheme.backgroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FlexibleSpaceBar(
|
||||||
|
title: Text(
|
||||||
|
realm.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
appbarColor.value ??
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor,
|
||||||
|
shadows: [iconShadow],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
background:
|
||||||
|
Container(), // Empty container since background is handled by Stack
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.people, shadows: [iconShadow]),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
_RealmMemberListSheet(realmSlug: slug),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_RealmActionMenu(realmSlug: slug, iconShadow: iconShadow),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverGap(4),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: realmIdentity.when(
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
data: (identity) => Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
realmDescriptionWidget(realm),
|
||||||
|
if (identity == null && realm.isCommunity)
|
||||||
|
realmActionWidget(realm)
|
||||||
|
else
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: realmChatRoomListWidget(realm)),
|
||||||
|
SliverPostList(
|
||||||
|
query: PostListQuery(realm: slug, pinned: true),
|
||||||
|
),
|
||||||
|
SliverPostList(
|
||||||
|
query: PostListQuery(realm: slug, pinned: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -391,135 +368,125 @@ 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: () {
|
context.pushReplacementNamed(
|
||||||
context.pushReplacementNamed(
|
'realmEdit',
|
||||||
'realmEdit',
|
pathParameters: {'slug': realmSlug},
|
||||||
pathParameters: {'slug': realmSlug},
|
);
|
||||||
);
|
},
|
||||||
},
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Icon(
|
||||||
Icon(
|
Icons.edit,
|
||||||
Icons.edit,
|
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
const Text('editRealm').tr(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const Gap(12),
|
||||||
realmIdentity.when(
|
const Text('editRealm').tr(),
|
||||||
data:
|
],
|
||||||
(identity) =>
|
),
|
||||||
(identity?.role ?? 0) >= 100
|
),
|
||||||
? PopupMenuItem(
|
realmIdentity.when(
|
||||||
child: Row(
|
data: (identity) => (identity?.role ?? 0) >= 100
|
||||||
children: [
|
? PopupMenuItem(
|
||||||
const Icon(Icons.delete, color: Colors.red),
|
child: Row(
|
||||||
const Gap(12),
|
children: [
|
||||||
const Text(
|
const Icon(Icons.delete, color: Colors.red),
|
||||||
'deleteRealm',
|
const Gap(12),
|
||||||
style: TextStyle(color: Colors.red),
|
const Text(
|
||||||
).tr(),
|
'deleteRealm',
|
||||||
],
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
).tr(),
|
||||||
onTap: () {
|
],
|
||||||
showConfirmAlert(
|
|
||||||
'deleteRealmHint'.tr(),
|
|
||||||
'deleteRealm'.tr(),
|
|
||||||
isDanger: true,
|
|
||||||
).then((confirm) {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
client.delete('/pass/realms/$realmSlug');
|
|
||||||
ref.invalidate(realmsJoinedProvider);
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.exit_to_app,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
const Gap(12),
|
|
||||||
Text(
|
|
||||||
'leaveRealm',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showConfirmAlert(
|
|
||||||
'leaveRealmHint'.tr(),
|
|
||||||
'leaveRealm'.tr(),
|
|
||||||
).then((confirm) async {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/pass/realms/$realmSlug/members/me',
|
|
||||||
);
|
|
||||||
ref.invalidate(realmsJoinedProvider);
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
() => const PopupMenuItem(
|
|
||||||
enabled: false,
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
),
|
||||||
error:
|
onTap: () {
|
||||||
(_, _) => PopupMenuItem(
|
showConfirmAlert(
|
||||||
child: Row(
|
'deleteRealmHint'.tr(),
|
||||||
children: [
|
'deleteRealm'.tr(),
|
||||||
Icon(
|
isDanger: true,
|
||||||
Icons.exit_to_app,
|
).then((confirm) {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
client.delete('/pass/realms/$realmSlug');
|
||||||
|
ref.invalidate(realmsJoinedProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.exit_to_app,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Text(
|
||||||
|
'leaveRealm',
|
||||||
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
const Gap(12),
|
).tr(),
|
||||||
Text(
|
],
|
||||||
'leaveRealm',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
showConfirmAlert(
|
|
||||||
'leaveRealmHint'.tr(),
|
|
||||||
'leaveRealm'.tr(),
|
|
||||||
).then((confirm) async {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/pass/realms/$realmSlug/members/me',
|
|
||||||
);
|
|
||||||
ref.invalidate(realmsJoinedProvider);
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pop(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
showConfirmAlert(
|
||||||
|
'leaveRealmHint'.tr(),
|
||||||
|
'leaveRealm'.tr(),
|
||||||
|
).then((confirm) async {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/pass/realms/$realmSlug/members/me',
|
||||||
|
);
|
||||||
|
ref.invalidate(realmsJoinedProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading: () => const PopupMenuItem(
|
||||||
|
enabled: false,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error: (_, _) => PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.exit_to_app,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Text(
|
||||||
|
'leaveRealm',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
onTap: () {
|
||||||
|
showConfirmAlert('leaveRealmHint'.tr(), 'leaveRealm'.tr()).then((
|
||||||
|
confirm,
|
||||||
|
) async {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.delete('/pass/realms/$realmSlug/members/me');
|
||||||
|
ref.invalidate(realmsJoinedProvider);
|
||||||
|
if (context.mounted) {
|
||||||
|
context.pop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,11 +651,10 @@ 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,
|
),
|
||||||
),
|
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
// Refresh the provider
|
// Refresh the provider
|
||||||
@@ -809,23 +775,19 @@ 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,
|
return TextField(
|
||||||
focusNode,
|
controller: controller,
|
||||||
onFieldSubmitted,
|
focusNode: focusNode,
|
||||||
) {
|
keyboardType: TextInputType.number,
|
||||||
return TextField(
|
decoration: InputDecoration(
|
||||||
controller: controller,
|
labelText: 'memberRole'.tr(),
|
||||||
focusNode: focusNode,
|
helperText: 'memberRoleHint'.tr(),
|
||||||
keyboardType: TextInputType.number,
|
),
|
||||||
decoration: InputDecoration(
|
onTapOutside: (event) => focusNode.unfocus(),
|
||||||
labelText: 'memberRole'.tr(),
|
);
|
||||||
helperText: 'memberRoleHint'.tr(),
|
},
|
||||||
),
|
|
||||||
onTapOutside: (event) => focusNode.unfocus(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
|
|||||||
@@ -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,10 +249,9 @@ 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;
|
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Seed Color').tr(),
|
title: Text('Seed Color').tr(),
|
||||||
@@ -292,10 +291,9 @@ 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,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
@@ -310,19 +308,17 @@ 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(),
|
|
||||||
),
|
),
|
||||||
// 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -333,10 +329,9 @@ 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -347,10 +342,9 @@ 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -361,10 +355,9 @@ 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -375,10 +368,9 @@ 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -391,10 +383,9 @@ 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) {
|
||||||
final current = settings.customColors ?? ThemeColors();
|
final current = settings.customColors ?? ThemeColors();
|
||||||
ref
|
ref
|
||||||
@@ -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();
|
||||||
@@ -598,8 +592,8 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
final color =
|
final color =
|
||||||
MediaQuery.of(context).platformBrightness == Brightness.dark
|
MediaQuery.of(context).platformBrightness == Brightness.dark
|
||||||
? colorScheme.primary
|
? colorScheme.primary
|
||||||
: colorScheme.primary;
|
: colorScheme.primary;
|
||||||
ref
|
ref
|
||||||
.read(appSettingsProvider.notifier)
|
.read(appSettingsProvider.notifier)
|
||||||
.setAppColorScheme(color.value);
|
.setAppColorScheme(color.value);
|
||||||
@@ -674,20 +668,19 @@ 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(
|
message: p.name,
|
||||||
message: p.name,
|
child: Text(
|
||||||
child: Text(
|
p.name,
|
||||||
p.name,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
).fontSize(14),
|
||||||
).fontSize(14),
|
),
|
||||||
),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
|
||||||
value: currentPoolId,
|
value: currentPoolId,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref
|
ref
|
||||||
@@ -705,19 +698,17 @@ 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: (err, st) => ListTile(
|
||||||
error:
|
minLeadingWidth: 48,
|
||||||
(err, st) => ListTile(
|
title: Text('settingsDefaultPool').tr(),
|
||||||
minLeadingWidth: 48,
|
subtitle: Text('Error: $err'),
|
||||||
title: Text('settingsDefaultPool').tr(),
|
leading: const Icon(Icons.error, color: Colors.red),
|
||||||
subtitle: Text('Error: $err'),
|
),
|
||||||
leading: const Icon(Icons.error, color: Colors.red),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -767,10 +758,9 @@ 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),
|
||||||
leading: const Icon(Symbols.send),
|
leading: const Icon(Symbols.send),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
@@ -823,33 +813,32 @@ class SettingsScreen extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Desktop-specific settings
|
// Desktop-specific settings
|
||||||
final desktopSettings =
|
final desktopSettings = !isDesktop
|
||||||
!isDesktop
|
? <Widget>[]
|
||||||
? <Widget>[]
|
: [
|
||||||
: [
|
ListTile(
|
||||||
ListTile(
|
minLeadingWidth: 48,
|
||||||
minLeadingWidth: 48,
|
title: Text('settingsWindowOpacity').tr(),
|
||||||
title: Text('settingsWindowOpacity').tr(),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
leading: const Icon(Symbols.opacity),
|
||||||
leading: const Icon(Symbols.opacity),
|
subtitle: Padding(
|
||||||
subtitle: Padding(
|
padding: const EdgeInsets.only(top: 8),
|
||||||
padding: const EdgeInsets.only(top: 8),
|
child: Slider(
|
||||||
child: Slider(
|
value: settings.windowOpacity,
|
||||||
value: settings.windowOpacity,
|
min: 0.1,
|
||||||
min: 0.1,
|
max: 1.0,
|
||||||
max: 1.0,
|
year2023: true,
|
||||||
year2023: true,
|
padding: EdgeInsets.only(right: 24),
|
||||||
padding: EdgeInsets.only(right: 24),
|
label: '${(settings.windowOpacity * 100).round()}%',
|
||||||
label: '${(settings.windowOpacity * 100).round()}%',
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
ref
|
||||||
ref
|
.read(appSettingsProvider.notifier)
|
||||||
.read(appSettingsProvider.notifier)
|
.setWindowOpacity(value);
|
||||||
.setWindowOpacity(value);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
),
|
||||||
|
];
|
||||||
|
|
||||||
// Create a responsive layout based on screen width
|
// Create a responsive layout based on screen width
|
||||||
Widget buildSettingsList() {
|
Widget buildSettingsList() {
|
||||||
|
|||||||
@@ -127,102 +127,88 @@ 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(
|
child: GridView.builder(
|
||||||
packId: id,
|
padding: const EdgeInsets.symmetric(
|
||||||
).future,
|
horizontal: 24,
|
||||||
),
|
vertical: 20,
|
||||||
child: GridView.builder(
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
gridDelegate:
|
||||||
horizontal: 24,
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
vertical: 20,
|
maxCrossAxisExtent: 96,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
),
|
),
|
||||||
gridDelegate:
|
itemCount: stickers.length,
|
||||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
itemBuilder: (context, index) {
|
||||||
maxCrossAxisExtent: 96,
|
final sticker = stickers[index];
|
||||||
mainAxisSpacing: 12,
|
return Tooltip(
|
||||||
crossAxisSpacing: 12,
|
message: ':${p?.prefix ?? ''}${sticker.slug}:',
|
||||||
),
|
child: ClipRRect(
|
||||||
itemCount: stickers.length,
|
borderRadius: const BorderRadius.all(
|
||||||
itemBuilder: (context, index) {
|
Radius.circular(8),
|
||||||
final sticker = stickers[index];
|
),
|
||||||
return Tooltip(
|
child: Container(
|
||||||
message: ':${p?.prefix ?? ''}${sticker.slug}:',
|
decoration: BoxDecoration(
|
||||||
child: ClipRRect(
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer,
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: Container(
|
),
|
||||||
decoration: BoxDecoration(
|
child: AspectRatio(
|
||||||
color:
|
aspectRatio: 1,
|
||||||
Theme.of(
|
child: CloudImageWidget(
|
||||||
context,
|
fileId: sticker.image.id,
|
||||||
).colorScheme.surfaceContainer,
|
fit: BoxFit.contain,
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: CloudImageWidget(
|
|
||||||
fileId: sticker.image.id,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
error:
|
),
|
||||||
(err, _) =>
|
),
|
||||||
Text(
|
error: (err, _) => Text(
|
||||||
'Error: $err',
|
'Error: $err',
|
||||||
).textAlignment(TextAlign.center).center(),
|
).textAlignment(TextAlign.center).center(),
|
||||||
loading: () => const CircularProgressIndicator().center(),
|
loading: () => const CircularProgressIndicator().center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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:
|
? removePackFromMyCollection
|
||||||
isOwned
|
: addPackToMyCollection,
|
||||||
? removePackFromMyCollection
|
icon: Icon(
|
||||||
: addPackToMyCollection,
|
isOwned ? Symbols.remove_circle : Symbols.add_circle,
|
||||||
icon: Icon(
|
),
|
||||||
isOwned ? Symbols.remove_circle : Symbols.add_circle,
|
label: Text(isOwned ? 'removePack'.tr() : 'addPack'.tr()),
|
||||||
),
|
),
|
||||||
label: Text(
|
loading: () => const SizedBox(
|
||||||
isOwned ? 'removePack'.tr() : 'addPack'.tr(),
|
height: 32,
|
||||||
),
|
width: 32,
|
||||||
),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
loading:
|
).center(),
|
||||||
() => const SizedBox(
|
error: (_, _) => OutlinedButton.icon(
|
||||||
height: 32,
|
onPressed: addPackToMyCollection,
|
||||||
width: 32,
|
icon: const Icon(Symbols.add_circle),
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
label: Text('addPack').tr(),
|
||||||
),
|
),
|
||||||
error:
|
|
||||||
(_, _) => OutlinedButton.icon(
|
|
||||||
onPressed: addPackToMyCollection,
|
|
||||||
icon: const Icon(Symbols.add_circle),
|
|
||||||
label: Text('addPack').tr(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,11 +59,10 @@ 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();
|
|
||||||
|
|
||||||
return stickers;
|
return stickers;
|
||||||
}
|
}
|
||||||
@@ -112,14 +110,12 @@ 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: query.value.byUsage
|
||||||
tooltip:
|
? 'orderByPopularity'.tr()
|
||||||
query.value.byUsage
|
: 'orderByReleaseDate'.tr(),
|
||||||
? 'orderByPopularity'.tr()
|
|
||||||
: 'orderByReleaseDate'.tr(),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
@@ -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,26 +167,53 @@ 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,
|
vertical: 20,
|
||||||
vertical: 20,
|
),
|
||||||
),
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(
|
||||||
|
math.min(pack.stickers.length, 4),
|
||||||
|
(index) => Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
right: index < 3 ? 8 : 0,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 80,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.tertiaryContainer,
|
||||||
|
),
|
||||||
|
child: CloudImageWidget(
|
||||||
|
file: pack.stickers[index].image,
|
||||||
|
),
|
||||||
|
).clipRRect(all: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (pack.stickers.length > 4)
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (pack.stickers.length > 4)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
math.min(pack.stickers.length, 4),
|
math.min(pack.stickers.length - 4, 4),
|
||||||
(index) => Padding(
|
(index) => Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: index < 3 ? 8 : 0,
|
right: index < 3 ? 8 : 0,
|
||||||
@@ -203,89 +226,55 @@ 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: pack.stickers[index].image,
|
file: pack.stickers[index + 4].image,
|
||||||
),
|
),
|
||||||
).clipRRect(all: 8),
|
).clipRRect(all: 8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (pack.stickers.length > 4)
|
],
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (pack.stickers.length > 4)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: List.generate(
|
|
||||||
math.min(pack.stickers.length - 4, 4),
|
|
||||||
(index) => Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
right: index < 3 ? 8 : 0,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 80,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
8,
|
|
||||||
),
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.tertiaryContainer,
|
|
||||||
),
|
|
||||||
child: CloudImageWidget(
|
|
||||||
file:
|
|
||||||
pack.stickers[index + 4].image,
|
|
||||||
),
|
|
||||||
).clipRRect(all: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).clipRRect(topLeft: 8, topRight: 8),
|
|
||||||
ListTile(
|
|
||||||
leading: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.tertiaryContainer,
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CloudImageWidget(
|
|
||||||
file: pack.icon ?? pack.stickers.first.image,
|
|
||||||
),
|
|
||||||
).width(40).height(40).clipRRect(all: 8),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(pack.name),
|
|
||||||
subtitle: Text(pack.description),
|
|
||||||
trailing: const Icon(Symbols.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
// Navigate to user-facing sticker pack detail page.
|
|
||||||
// Adjust the route name/parameters if your app uses different ones.
|
|
||||||
context.pushNamed(
|
|
||||||
'stickerPackDetail',
|
|
||||||
pathParameters: {'packId': pack.id},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
).clipRRect(topLeft: 8, topRight: 8),
|
||||||
|
ListTile(
|
||||||
|
leading: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.tertiaryContainer,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CloudImageWidget(
|
||||||
|
file: pack.icon ?? pack.stickers.firstOrNull?.image,
|
||||||
|
),
|
||||||
|
).width(40).height(40).clipRRect(all: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(pack.name),
|
||||||
|
subtitle: Text(pack.description),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// Navigate to user-facing sticker pack detail page.
|
||||||
|
// Adjust the route name/parameters if your app uses different ones.
|
||||||
|
context.pushNamed(
|
||||||
|
'stickerPackDetail',
|
||||||
|
pathParameters: {'packId': pack.id},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -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,13 +79,12 @@ 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),
|
);
|
||||||
);
|
}).toList(),
|
||||||
}).toList(),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedPoolId = value;
|
selectedPoolId = value;
|
||||||
@@ -140,10 +139,9 @@ 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),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -155,23 +153,22 @@ 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,28 +180,28 @@ 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (!typeAccepted) ...[
|
if (!typeAccepted) ...[
|
||||||
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,10 +226,9 @@ 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);
|
||||||
|
|||||||
@@ -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,45 +123,39 @@ 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
|
: _buildUnindexedFileListContent(
|
||||||
? SliverToBoxAdapter(
|
data,
|
||||||
child: _buildEmptyUnindexedFilesHint(ref),
|
ref,
|
||||||
)
|
context,
|
||||||
: _buildUnindexedFileListContent(
|
viewMode,
|
||||||
data,
|
isSelectionMode,
|
||||||
ref,
|
selectedFileIds,
|
||||||
context,
|
currentVisibleItems,
|
||||||
viewMode,
|
footer,
|
||||||
isSelectionMode,
|
),
|
||||||
selectedFileIds,
|
|
||||||
currentVisibleItems,
|
|
||||||
footer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_ => PaginationWidget(
|
_ => PaginationWidget(
|
||||||
provider: indexedCloudFileListProvider,
|
provider: indexedCloudFileListProvider,
|
||||||
notifier: indexedCloudFileListProvider.notifier,
|
notifier: indexedCloudFileListProvider.notifier,
|
||||||
isRefreshable: false,
|
isRefreshable: false,
|
||||||
isSliver: true,
|
isSliver: true,
|
||||||
contentBuilder:
|
contentBuilder: (data, footer) => data.isEmpty
|
||||||
(data, footer) =>
|
? SliverToBoxAdapter(
|
||||||
data.isEmpty
|
child: _buildEmptyDirectoryHint(ref, currentPath),
|
||||||
? SliverToBoxAdapter(
|
)
|
||||||
child: _buildEmptyDirectoryHint(ref, currentPath),
|
: _buildFileListContent(
|
||||||
)
|
data,
|
||||||
: _buildFileListContent(
|
ref,
|
||||||
data,
|
context,
|
||||||
ref,
|
currentPath,
|
||||||
context,
|
viewMode,
|
||||||
currentPath,
|
isSelectionMode,
|
||||||
viewMode,
|
selectedFileIds,
|
||||||
isSelectionMode,
|
currentVisibleItems,
|
||||||
selectedFileIds,
|
footer,
|
||||||
currentVisibleItems,
|
),
|
||||||
footer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,11 +171,10 @@ 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();
|
|
||||||
final breadcrumbs = <Widget>[];
|
final breadcrumbs = <Widget>[];
|
||||||
|
|
||||||
// Add root
|
// Add root
|
||||||
@@ -266,10 +259,9 @@ 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(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -302,28 +294,25 @@ 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 = currentPath.value
|
||||||
final pathParts =
|
.split('/')
|
||||||
currentPath.value
|
.where((part) => part.isNotEmpty)
|
||||||
.split('/')
|
.toList();
|
||||||
.where((part) => part.isNotEmpty)
|
if (pathParts.isNotEmpty) {
|
||||||
.toList();
|
pathParts.removeLast();
|
||||||
if (pathParts.isNotEmpty) {
|
currentPath.value = pathParts.isEmpty
|
||||||
pathParts.removeLast();
|
? '/'
|
||||||
currentPath.value =
|
: '/${pathParts.join('/')}';
|
||||||
pathParts.isEmpty
|
|
||||||
? '/'
|
|
||||||
: '/${pathParts.join('/')}';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
vertical: -4,
|
vertical: -4,
|
||||||
@@ -342,16 +331,13 @@ class FileListView extends HookConsumerWidget {
|
|||||||
? Symbols.view_module
|
? Symbols.view_module
|
||||||
: Symbols.list,
|
: Symbols.list,
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed: () => viewMode.value =
|
||||||
() =>
|
|
||||||
viewMode.value =
|
|
||||||
viewMode.value == FileListViewMode.list
|
|
||||||
? FileListViewMode.waterfall
|
|
||||||
: FileListViewMode.list,
|
|
||||||
tooltip:
|
|
||||||
viewMode.value == FileListViewMode.list
|
viewMode.value == FileListViewMode.list
|
||||||
? 'Switch to Waterfall View'
|
? FileListViewMode.waterfall
|
||||||
: 'Switch to List View',
|
: FileListViewMode.list,
|
||||||
|
tooltip: viewMode.value == FileListViewMode.list
|
||||||
|
? 'Switch to Waterfall View'
|
||||||
|
: 'Switch to List View',
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
vertical: -4,
|
vertical: -4,
|
||||||
@@ -363,12 +349,11 @@ 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(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
vertical: -4,
|
vertical: -4,
|
||||||
@@ -377,9 +362,8 @@ 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(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
@@ -397,10 +381,9 @@ 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(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
vertical: -4,
|
vertical: -4,
|
||||||
@@ -429,12 +412,14 @@ 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:
|
||||||
slivers: [bodyWidget, const SliverGap(12)],
|
CustomScrollView(
|
||||||
).padding(
|
slivers: [bodyWidget, const SliverGap(12)],
|
||||||
horizontal:
|
).padding(
|
||||||
viewMode.value == FileListViewMode.waterfall ? 12 : null,
|
horizontal: viewMode.value == FileListViewMode.waterfall
|
||||||
),
|
? 12
|
||||||
|
: null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (isSelectionMode.value)
|
if (isSelectionMode.value)
|
||||||
Material(
|
Material(
|
||||||
@@ -457,16 +442,15 @@ 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],
|
unindexedFile: (u) => [u.file.id],
|
||||||
unindexedFile: (u) => [u.file.id],
|
orElse: () => <String>[],
|
||||||
orElse: () => <String>[],
|
),
|
||||||
),
|
)
|
||||||
)
|
.toSet();
|
||||||
.toSet();
|
|
||||||
|
|
||||||
if (allIds
|
if (allIds
|
||||||
.difference(selectedFileIds.value)
|
.difference(selectedFileIds.value)
|
||||||
@@ -482,16 +466,16 @@ class FileListView extends HookConsumerWidget {
|
|||||||
currentVisibleItems.value.isEmpty
|
currentVisibleItems.value.isEmpty
|
||||||
? 'Select All'
|
? 'Select All'
|
||||||
: currentVisibleItems.value
|
: currentVisibleItems.value
|
||||||
.expand(
|
.expand(
|
||||||
(item) => item.maybeMap(
|
(item) => item.maybeMap(
|
||||||
file: (f) => [f.fileIndex.id],
|
file: (f) => [f.fileIndex.id],
|
||||||
unindexedFile: (u) => [u.file.id],
|
unindexedFile: (u) => [u.file.id],
|
||||||
orElse: () => <String>[],
|
orElse: () => <String>[],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toSet()
|
.toSet()
|
||||||
.difference(selectedFileIds.value)
|
.difference(selectedFileIds.value)
|
||||||
.isEmpty
|
.isEmpty
|
||||||
? 'Deselect All'
|
? 'Deselect All'
|
||||||
: 'Select All',
|
: 'Select All',
|
||||||
),
|
),
|
||||||
@@ -502,47 +486,46 @@ 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?',
|
'Delete Selected Files',
|
||||||
'Delete Selected Files',
|
isDanger: true,
|
||||||
isDanger: true,
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
if (context.mounted) {
|
||||||
|
showLoadingModal(context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.post(
|
||||||
|
'/drive/files/batches/delete',
|
||||||
|
data: {
|
||||||
|
'file_ids': selectedFileIds.value
|
||||||
|
.toList(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
final count = resp.data['count'] as int;
|
||||||
|
selectedFileIds.value.clear();
|
||||||
|
isSelectionMode.value = false;
|
||||||
|
ref.invalidate(
|
||||||
|
mode.value == FileListMode.normal
|
||||||
|
? indexedCloudFileListProvider
|
||||||
|
: unindexedFileListProvider,
|
||||||
|
);
|
||||||
|
showSnackBar('Deleted $count files.');
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar(
|
||||||
|
'Failed to delete selected files.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
}
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
final resp = await client.post(
|
|
||||||
'/drive/files/batches/delete',
|
|
||||||
data: {
|
|
||||||
'file_ids':
|
|
||||||
selectedFileIds.value.toList(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final count = resp.data['count'] as int;
|
|
||||||
selectedFileIds.value.clear();
|
|
||||||
isSelectionMode.value = false;
|
|
||||||
ref.invalidate(
|
|
||||||
mode.value == FileListMode.normal
|
|
||||||
? indexedCloudFileListProvider
|
|
||||||
: unindexedFileListProvider,
|
|
||||||
);
|
|
||||||
showSnackBar('Deleted $count files.');
|
|
||||||
} catch (e) {
|
|
||||||
showSnackBar(
|
|
||||||
'Failed to delete selected files.',
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) {
|
|
||||||
hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -584,26 +567,24 @@ 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,
|
isSelectionMode.value,
|
||||||
isSelectionMode.value,
|
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||||
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
() {
|
||||||
() {
|
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||||
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
..remove(fileItem.fileIndex.id);
|
||||||
..remove(fileItem.fileIndex.id);
|
} else {
|
||||||
} else {
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
..add(fileItem.fileIndex.id);
|
||||||
..add(fileItem.fileIndex.id);
|
}
|
||||||
}
|
},
|
||||||
},
|
),
|
||||||
),
|
folder: (folderItem) =>
|
||||||
folder:
|
_buildWaterfallFolderTile(folderItem, currentPath, context),
|
||||||
(folderItem) =>
|
|
||||||
_buildWaterfallFolderTile(folderItem, currentPath, context),
|
|
||||||
unindexedFile: (unindexedFileItem) {
|
unindexedFile: (unindexedFileItem) {
|
||||||
// Should not happen
|
// Should not happen
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -620,47 +601,44 @@ 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,
|
isSelectionMode.value,
|
||||||
isSelectionMode.value,
|
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||||
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
() {
|
||||||
() {
|
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||||
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
..remove(fileItem.fileIndex.id);
|
||||||
..remove(fileItem.fileIndex.id);
|
} else {
|
||||||
} else {
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
..add(fileItem.fileIndex.id);
|
||||||
..add(fileItem.fileIndex.id);
|
}
|
||||||
}
|
},
|
||||||
},
|
),
|
||||||
),
|
folder: (folderItem) => ListTile(
|
||||||
folder:
|
leading: ClipRRect(
|
||||||
(folderItem) => ListTile(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
leading: ClipRRect(
|
child: SizedBox(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
height: 48,
|
||||||
child: SizedBox(
|
width: 48,
|
||||||
height: 48,
|
child: const Icon(Symbols.folder, fill: 1).center(),
|
||||||
width: 48,
|
|
||||||
child: const Icon(Symbols.folder, fill: 1).center(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
folderItem.folderName,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: const Text('folder').tr(),
|
|
||||||
onTap: () {
|
|
||||||
final newPath =
|
|
||||||
currentPath.value == '/'
|
|
||||||
? '/${folderItem.folderName}'
|
|
||||||
: '${currentPath.value}/${folderItem.folderName}';
|
|
||||||
currentPath.value = newPath;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
folderItem.folderName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
subtitle: const Text('folder').tr(),
|
||||||
|
onTap: () {
|
||||||
|
final newPath = currentPath.value == '/'
|
||||||
|
? '/${folderItem.folderName}'
|
||||||
|
: '${currentPath.value}/${folderItem.folderName}';
|
||||||
|
currentPath.value = newPath;
|
||||||
|
},
|
||||||
|
),
|
||||||
unindexedFile: (unindexedFileItem) {
|
unindexedFile: (unindexedFileItem) {
|
||||||
// Should not happen in normal mode
|
// Should not happen in normal mode
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -705,10 +683,9 @@ 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(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -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,22 +829,20 @@ 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) =>
|
? SingleChildScrollView(
|
||||||
snapshot.hasData
|
padding: EdgeInsets.all(24),
|
||||||
? SingleChildScrollView(
|
child: Text(
|
||||||
padding: EdgeInsets.all(24),
|
snapshot.data!,
|
||||||
child: Text(
|
style: const TextStyle(
|
||||||
snapshot.data!,
|
fontSize: 9,
|
||||||
style: const TextStyle(
|
fontFamily: 'monospace',
|
||||||
fontSize: 9,
|
),
|
||||||
fontFamily: 'monospace',
|
maxLines: 20,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 20,
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
)
|
||||||
),
|
: const Center(child: CircularProgressIndicator()),
|
||||||
)
|
|
||||||
: const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -961,10 +937,9 @@ 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;
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -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,25 +1052,22 @@ 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(unindexedFileItem.file.id)) {
|
||||||
if (selectedFileIds.value.contains(
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
unindexedFileItem.file.id,
|
..remove(unindexedFileItem.file.id);
|
||||||
)) {
|
} else {
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
..remove(unindexedFileItem.file.id);
|
..add(unindexedFileItem.file.id);
|
||||||
} else {
|
}
|
||||||
selectedFileIds.value = Set.from(selectedFileIds.value)
|
},
|
||||||
..add(unindexedFileItem.file.id);
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1130,10 +1102,9 @@ 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)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
@@ -1199,10 +1170,9 @@ 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)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
@@ -1289,10 +1259,9 @@ 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(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -1395,19 +1364,18 @@ 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)),
|
),
|
||||||
),
|
...pools.map(
|
||||||
...pools.map(
|
(p) => DropdownMenuItem<SnFilePool>(
|
||||||
(p) => DropdownMenuItem<SnFilePool>(
|
value: p,
|
||||||
value: p,
|
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||||
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||||
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||||
);
|
);
|
||||||
@@ -1416,17 +1384,16 @@ 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;
|
if (mode.value == FileListMode.unindexed) {
|
||||||
if (mode.value == FileListMode.unindexed) {
|
unindexedNotifier.setPool(value?.id);
|
||||||
unindexedNotifier.setPool(value?.id);
|
} else {
|
||||||
} else {
|
cloudNotifier.setPool(value?.id);
|
||||||
cloudNotifier.setPool(value?.id);
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
customButton: Container(
|
customButton: Container(
|
||||||
height: 28,
|
height: 28,
|
||||||
width: 200,
|
width: 200,
|
||||||
@@ -1493,19 +1460,17 @@ 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: Text(
|
||||||
child:
|
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
|
||||||
Text(
|
style: const TextStyle(fontSize: 14),
|
||||||
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
|
).tr(),
|
||||||
style: const TextStyle(fontSize: 14),
|
),
|
||||||
).tr(),
|
)
|
||||||
),
|
.toList(),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (value) => order.value = value,
|
onChanged: (value) => order.value = value,
|
||||||
customButton: Container(
|
customButton: Container(
|
||||||
height: 28,
|
height: 28,
|
||||||
@@ -1517,13 +1482,12 @@ 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()}',
|
style: const TextStyle(fontSize: 12),
|
||||||
style: const TextStyle(fontSize: 12),
|
).tr(),
|
||||||
).tr(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
buttonStyleData: const ButtonStyleData(
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
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_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';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.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:super_sliver_list/super_sliver_list.dart';
|
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
@@ -15,19 +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 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.footerSkeletonMaxWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -35,48 +46,149 @@ 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) {
|
||||||
final content = ResponseErrorWidget(
|
final content = ResponseErrorWidget(
|
||||||
error: data.error,
|
error: data.error,
|
||||||
onRetry: noti.refresh,
|
onRetry: noti.refresh,
|
||||||
);
|
);
|
||||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
return SliverFillRemaining(child: content);
|
||||||
}
|
}
|
||||||
|
|
||||||
final listView =
|
final listView = SuperSliverList.separated(
|
||||||
isSliver
|
itemCount: (data.value?.length ?? 0) + 1,
|
||||||
? SuperSliverList.builder(
|
itemBuilder: (context, idx) {
|
||||||
itemCount: (data.value?.length ?? 0) + 1,
|
if (idx == data.value?.length) {
|
||||||
itemBuilder: (context, idx) {
|
return PaginationListFooter(
|
||||||
if (idx == data.value?.length) {
|
noti: noti,
|
||||||
return PaginationListFooter(noti: noti, data: data);
|
data: data,
|
||||||
}
|
skeletonChild: footerSkeletonChild,
|
||||||
final entry = data.value?[idx];
|
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||||
if (entry != null) return itemBuilder(context, idx, entry);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: SuperListView.builder(
|
|
||||||
padding: padding,
|
|
||||||
itemCount: (data.value?.length ?? 0) + 1,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
if (idx == data.value?.length) {
|
|
||||||
return PaginationListFooter(noti: noti, data: data);
|
|
||||||
}
|
|
||||||
final entry = data.value?[idx];
|
|
||||||
if (entry != null) return itemBuilder(context, idx, entry);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
final entry = data.value?[idx];
|
||||||
|
if (entry != null) return itemBuilder(context, idx, entry);
|
||||||
|
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 isRefreshable
|
||||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
||||||
: 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,
|
||||||
|
itemCount: (data.value?.length ?? 0) + 1,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
if (idx == data.value?.length) {
|
||||||
|
return PaginationListFooter(
|
||||||
|
noti: noti,
|
||||||
|
data: data,
|
||||||
|
skeletonChild: footerSkeletonChild,
|
||||||
|
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final entry = data.value?[idx];
|
||||||
|
if (entry != null) return itemBuilder(context, idx, entry);
|
||||||
|
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 SizedBox(
|
||||||
|
key: const ValueKey('data'),
|
||||||
|
child: isRefreshable
|
||||||
|
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
||||||
|
: listView,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: buildContent(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +199,8 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
|||||||
final bool isRefreshable;
|
final bool isRefreshable;
|
||||||
final bool isSliver;
|
final bool isSliver;
|
||||||
final bool showDefaultWidgets;
|
final bool showDefaultWidgets;
|
||||||
|
final Widget? footerSkeletonChild;
|
||||||
|
final double? footerSkeletonMaxWidth;
|
||||||
const PaginationWidget({
|
const PaginationWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.provider,
|
required this.provider,
|
||||||
@@ -95,6 +209,8 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
|||||||
this.isRefreshable = true,
|
this.isRefreshable = true,
|
||||||
this.isSliver = false,
|
this.isSliver = false,
|
||||||
this.showDefaultWidgets = true,
|
this.showDefaultWidgets = true,
|
||||||
|
this.footerSkeletonChild,
|
||||||
|
this.footerSkeletonMaxWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -102,62 +218,151 @@ 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) {
|
||||||
final content = ResponseErrorWidget(
|
final content = ResponseErrorWidget(
|
||||||
error: data.error,
|
error: data.error,
|
||||||
onRetry: noti.refresh,
|
onRetry: noti.refresh,
|
||||||
|
);
|
||||||
|
return SliverFillRemaining(child: content);
|
||||||
|
}
|
||||||
|
|
||||||
|
final footer = PaginationListFooter(
|
||||||
|
noti: noti,
|
||||||
|
data: data,
|
||||||
|
skeletonChild: footerSkeletonChild,
|
||||||
|
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||||
);
|
);
|
||||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
final content = contentBuilder(data.value ?? [], footer);
|
||||||
|
|
||||||
|
return isRefreshable
|
||||||
|
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
|
||||||
|
: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
final footer = PaginationListFooter(noti: noti, data: data);
|
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
|
||||||
final content = contentBuilder(data.value ?? [], footer);
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return isRefreshable
|
if (data.hasError) {
|
||||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
|
final content = ResponseErrorWidget(
|
||||||
: content;
|
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 StatelessWidget {
|
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 double? skeletonMaxWidth;
|
||||||
final bool isSliver;
|
final bool isSliver;
|
||||||
|
|
||||||
const PaginationListFooter({
|
const PaginationListFooter({
|
||||||
super.key,
|
super.key,
|
||||||
required this.noti,
|
required this.noti,
|
||||||
required this.data,
|
required this.data,
|
||||||
|
this.skeletonChild,
|
||||||
|
this.skeletonMaxWidth,
|
||||||
this.isSliver = false,
|
this.isSliver = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final child = SizedBox(
|
final hasBeenVisible = useState(false);
|
||||||
height: 64,
|
|
||||||
child: Center(
|
final placeholder = Skeletonizer(
|
||||||
child:
|
enabled: true,
|
||||||
data.isLoading
|
effect: ShimmerEffect(
|
||||||
? CircularProgressIndicator()
|
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
: Row(
|
highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
child: skeletonChild ?? _DefaultSkeletonChild(maxWidth: skeletonMaxWidth),
|
||||||
|
);
|
||||||
|
final child = hasBeenVisible.value
|
||||||
|
? data.isLoading
|
||||||
|
? placeholder
|
||||||
|
: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.close, size: 16),
|
const Icon(Symbols.close, size: 16),
|
||||||
Text('noFurtherData').tr().fontSize(13),
|
Text('noFurtherData').tr().fontSize(13),
|
||||||
],
|
],
|
||||||
).opacity(0.9),
|
).opacity(0.9).height(64).center()
|
||||||
).padding(all: 8),
|
: placeholder;
|
||||||
);
|
|
||||||
|
|
||||||
return VisibilityDetector(
|
return VisibilityDetector(
|
||||||
key: Key("pagination-list-${noti.hashCode}"),
|
key: Key("pagination-list-${noti.hashCode}"),
|
||||||
onVisibilityChanged: (VisibilityInfo info) {
|
onVisibilityChanged: (VisibilityInfo info) {
|
||||||
|
hasBeenVisible.value = true;
|
||||||
if (!noti.fetchedAll && !data.isLoading && !data.hasError) {
|
if (!noti.fetchedAll && !data.isLoading && !data.hasError) {
|
||||||
noti.fetchFurther();
|
noti.fetchFurther();
|
||||||
}
|
}
|
||||||
@@ -166,3 +371,26 @@ class PaginationListFooter<T> extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,39 +102,38 @@ 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,
|
children: [
|
||||||
children: [
|
buildVisibilityOption(
|
||||||
buildVisibilityOption(
|
context,
|
||||||
context,
|
0,
|
||||||
0,
|
Symbols.public,
|
||||||
Symbols.public,
|
'postVisibilityPublic',
|
||||||
'postVisibilityPublic',
|
|
||||||
),
|
|
||||||
buildVisibilityOption(
|
|
||||||
context,
|
|
||||||
1,
|
|
||||||
Symbols.group,
|
|
||||||
'postVisibilityFriends',
|
|
||||||
),
|
|
||||||
buildVisibilityOption(
|
|
||||||
context,
|
|
||||||
2,
|
|
||||||
Symbols.link_off,
|
|
||||||
'postVisibilityUnlisted',
|
|
||||||
),
|
|
||||||
buildVisibilityOption(
|
|
||||||
context,
|
|
||||||
3,
|
|
||||||
Symbols.lock,
|
|
||||||
'postVisibilityPrivate',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
1,
|
||||||
|
Symbols.group,
|
||||||
|
'postVisibilityFriends',
|
||||||
|
),
|
||||||
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
2,
|
||||||
|
Symbols.link_off,
|
||||||
|
'postVisibilityUnlisted',
|
||||||
|
),
|
||||||
|
buildVisibilityOption(
|
||||||
|
context,
|
||||||
|
3,
|
||||||
|
Symbols.lock,
|
||||||
|
'postVisibilityPrivate',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,51 +189,48 @@ 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,
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderRadius: BorderRadius.circular(16),
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'#$tag',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
const Gap(4),
|
||||||
horizontal: 12,
|
InkWell(
|
||||||
vertical: 6,
|
onTap: () {
|
||||||
|
final newTags = List<String>.from(
|
||||||
|
state.tags.value,
|
||||||
|
)..remove(tag);
|
||||||
|
state.tags.value = newTags;
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 16,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
],
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
);
|
||||||
Text(
|
}).toList(),
|
||||||
'#$tag',
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
InkWell(
|
|
||||||
onTap: () {
|
|
||||||
final newTags = List<String>.from(
|
|
||||||
state.tags.value,
|
|
||||||
)..remove(tag);
|
|
||||||
state.tags.value = newTags;
|
|
||||||
},
|
|
||||||
child: Icon(
|
|
||||||
Icons.close,
|
|
||||||
size: 16,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
// Tag input with autocomplete
|
// Tag input with autocomplete
|
||||||
TypeAheadField<SnPostTag>(
|
TypeAheadField<SnPostTag>(
|
||||||
@@ -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,55 +291,49 @@ 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(item);
|
||||||
final isSelected = state.categories.value.contains(
|
return InkWell(
|
||||||
item,
|
onTap: () {
|
||||||
);
|
isSelected
|
||||||
return InkWell(
|
? state.categories.value = state.categories.value
|
||||||
onTap: () {
|
.where((e) => e != item)
|
||||||
isSelected
|
.toList()
|
||||||
? state.categories.value =
|
: state.categories.value = [
|
||||||
state.categories.value
|
...state.categories.value,
|
||||||
.where((e) => e != item)
|
item,
|
||||||
.toList()
|
];
|
||||||
: state.categories.value = [
|
menuSetState(() {});
|
||||||
...state.categories.value,
|
|
||||||
item,
|
|
||||||
];
|
|
||||||
menuSetState(() {});
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
if (isSelected)
|
|
||||||
const Icon(Icons.check_box_outlined)
|
|
||||||
else
|
|
||||||
const Icon(Icons.check_box_outline_blank),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
item.categoryDisplayTitle,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
child: Container(
|
||||||
);
|
height: double.infinity,
|
||||||
}).toList(),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (isSelected)
|
||||||
|
const Icon(Icons.check_box_outlined)
|
||||||
|
else
|
||||||
|
const Icon(Icons.check_box_outline_blank),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item.categoryDisplayTitle,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
value: currentCategories.isEmpty ? null : currentCategories.last,
|
value: currentCategories.isEmpty ? null : currentCategories.last,
|
||||||
onChanged: (_) {},
|
onChanged: (_) {},
|
||||||
selectedItemBuilder: (context) {
|
selectedItemBuilder: (context) {
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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,11 +202,10 @@ 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;
|
|
||||||
if (cloudFile != null) {
|
if (cloudFile != null) {
|
||||||
// Update attachments list with cloud file
|
// Update attachments list with cloud file
|
||||||
final clone = List.of(state.attachments.value);
|
final clone = List.of(state.attachments.value);
|
||||||
@@ -242,11 +241,10 @@ 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(),
|
|
||||||
publisher: SnPublisher(
|
publisher: SnPublisher(
|
||||||
id: '',
|
id: '',
|
||||||
type: 0,
|
type: 0,
|
||||||
@@ -315,11 +313,10 @@ 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(),
|
|
||||||
publisher: SnPublisher(
|
publisher: SnPublisher(
|
||||||
id: '',
|
id: '',
|
||||||
type: 0,
|
type: 0,
|
||||||
@@ -501,11 +498,10 @@ 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> uploadAttachment(
|
static Future<void> uploadAttachment(
|
||||||
@@ -528,22 +524,20 @@ 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: attachment.type == UniversalFileType.file
|
||||||
mode:
|
? FileUploadMode.generic
|
||||||
attachment.type == UniversalFileType.file
|
: FileUploadMode.mediaSafe,
|
||||||
? FileUploadMode.generic
|
onProgress: (progress, _) {
|
||||||
: FileUploadMode.mediaSafe,
|
state.attachmentProgress.value = {
|
||||||
onProgress: (progress, _) {
|
...state.attachmentProgress.value,
|
||||||
state.attachmentProgress.value = {
|
index: progress ?? 0.0,
|
||||||
...state.attachmentProgress.value,
|
};
|
||||||
index: progress ?? 0.0,
|
},
|
||||||
};
|
).future;
|
||||||
},
|
|
||||||
).future;
|
|
||||||
|
|
||||||
if (cloudFile == null) {
|
if (cloudFile == null) {
|
||||||
throw ArgumentError('Failed to upload the file...');
|
throw ArgumentError('Failed to upload the file...');
|
||||||
@@ -713,11 +707,10 @@ 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(),
|
|
||||||
'type': state.postType,
|
'type': state.postType,
|
||||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||||
|
|||||||
@@ -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,16 +37,21 @@ 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,10 +62,9 @@ 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);
|
|
||||||
|
|
||||||
// Use the full post data if available, otherwise fall back to originalPost
|
// Use the full post data if available, otherwise fall back to originalPost
|
||||||
final effectiveOriginalPost = fullPostData.when(
|
final effectiveOriginalPost = fullPostData.when(
|
||||||
@@ -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() {
|
||||||
@@ -145,26 +158,31 @@ class PostComposeSheet extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
(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,
|
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
)
|
||||||
)
|
: Icon(
|
||||||
: Icon(
|
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
||||||
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
),
|
||||||
),
|
tooltip: effectiveOriginalPost != null
|
||||||
tooltip:
|
? 'postUpdate'.tr()
|
||||||
effectiveOriginalPost != null
|
: 'postPublish'.tr(),
|
||||||
? 'postUpdate'.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,29 +210,28 @@ 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,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Text('restoreDraftMessage'.tr()),
|
||||||
Text('restoreDraftMessage'.tr()),
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 16),
|
_buildCompactDraftPreview(context, latestDraft),
|
||||||
_buildCompactDraftPreview(context, latestDraft),
|
],
|
||||||
],
|
),
|
||||||
),
|
actions: [
|
||||||
actions: [
|
TextButton(
|
||||||
TextButton(
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
child: Text('no'.tr()),
|
||||||
child: Text('no'.tr()),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text('yes'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: Text('yes'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (restore == true) {
|
if (restore == true) {
|
||||||
// Delete the old draft
|
// Delete the old draft
|
||||||
@@ -226,10 +243,9 @@ 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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
437
lib/widgets/post/post_item_skeleton.dart
Normal file
437
lib/widgets/post/post_item_skeleton.dart
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
class PostItemSkeleton extends StatelessWidget {
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final bool isFullPost;
|
||||||
|
final bool isShowReference;
|
||||||
|
final bool isEmbedReply;
|
||||||
|
final bool isCompact;
|
||||||
|
final double? borderRadius;
|
||||||
|
|
||||||
|
const PostItemSkeleton({
|
||||||
|
super.key,
|
||||||
|
this.padding,
|
||||||
|
this.isFullPost = false,
|
||||||
|
this.isShowReference = false,
|
||||||
|
this.isEmbedReply = false,
|
||||||
|
this.isCompact = false,
|
||||||
|
this.borderRadius,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final renderingPadding =
|
||||||
|
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.only(bottom: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Gap(renderingPadding.vertical),
|
||||||
|
_PostHeaderSkeleton(
|
||||||
|
isFullPost: isFullPost,
|
||||||
|
isCompact: isCompact,
|
||||||
|
renderingPadding: renderingPadding,
|
||||||
|
),
|
||||||
|
_PostBodySkeleton(
|
||||||
|
isFullPost: isFullPost,
|
||||||
|
renderingPadding: renderingPadding,
|
||||||
|
),
|
||||||
|
if (isShowReference)
|
||||||
|
_ReferencedPostWidgetSkeleton(
|
||||||
|
renderingPadding: renderingPadding,
|
||||||
|
),
|
||||||
|
if (isEmbedReply)
|
||||||
|
_PostReplyPreviewSkeleton(
|
||||||
|
renderingPadding: renderingPadding,
|
||||||
|
).padding(horizontal: renderingPadding.horizontal, top: 8),
|
||||||
|
Gap(renderingPadding.vertical),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostHeaderSkeleton extends StatelessWidget {
|
||||||
|
final bool isFullPost;
|
||||||
|
final bool isCompact;
|
||||||
|
final EdgeInsets renderingPadding;
|
||||||
|
|
||||||
|
const _PostHeaderSkeleton({
|
||||||
|
required this.isFullPost,
|
||||||
|
required this.isCompact,
|
||||||
|
required this.renderingPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
// Profile picture skeleton
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
// Name skeleton
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isCompact)
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 80,
|
||||||
|
margin: const EdgeInsets.only(left: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
// Timestamp skeleton
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Reaction button skeleton
|
||||||
|
if (!isCompact)
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: renderingPadding.horizontal, bottom: 4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostBodySkeleton extends StatelessWidget {
|
||||||
|
final bool isFullPost;
|
||||||
|
final EdgeInsets renderingPadding;
|
||||||
|
|
||||||
|
const _PostBodySkeleton({
|
||||||
|
required this.isFullPost,
|
||||||
|
required this.renderingPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Title skeleton (if applicable)
|
||||||
|
if (isFullPost)
|
||||||
|
Container(
|
||||||
|
height: 20,
|
||||||
|
width: 200,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: renderingPadding.horizontal,
|
||||||
|
right: renderingPadding.horizontal,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Content skeleton
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: renderingPadding.horizontal,
|
||||||
|
right: renderingPadding.horizontal,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 250,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: renderingPadding.horizontal,
|
||||||
|
right: renderingPadding.horizontal,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 180,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
left: renderingPadding.horizontal,
|
||||||
|
right: renderingPadding.horizontal,
|
||||||
|
bottom: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Metadata skeleton
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: renderingPadding.horizontal + 4, top: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReferencedPostWidgetSkeleton extends StatelessWidget {
|
||||||
|
final EdgeInsets renderingPadding;
|
||||||
|
|
||||||
|
const _ReferencedPostWidgetSkeleton({required this.renderingPadding});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: renderingPadding.horizontal,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
top: 8,
|
||||||
|
left: renderingPadding.vertical,
|
||||||
|
right: renderingPadding.vertical,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 150,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostReplyPreviewSkeleton extends StatelessWidget {
|
||||||
|
final EdgeInsets renderingPadding;
|
||||||
|
|
||||||
|
const _PostReplyPreviewSkeleton({required this.renderingPadding});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 14,
|
||||||
|
width: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
width: 150,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
height: 10,
|
||||||
|
width: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,79 +1,12 @@
|
|||||||
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';
|
||||||
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 {
|
||||||
@@ -85,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;
|
||||||
@@ -107,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,
|
||||||
@@ -131,34 +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: 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(
|
||||||
|
|||||||
@@ -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`
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,39 +118,49 @@ 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: Row(
|
child: ConstrainedBox(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
spacing: 8,
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
ProfilePictureWidget(
|
spacing: 8,
|
||||||
file: post.publisher.picture,
|
children: [
|
||||||
radius: 12,
|
ProfilePictureWidget(
|
||||||
).padding(top: 4),
|
file: post.publisher.picture,
|
||||||
if (post.content?.isNotEmpty ?? false)
|
radius: 12,
|
||||||
Expanded(
|
).padding(top: 4),
|
||||||
child: MarkdownTextContent(
|
if (post.content?.isNotEmpty ?? false)
|
||||||
content: post.content!,
|
Expanded(
|
||||||
attachments: post.attachments,
|
child: MarkdownTextContent(
|
||||||
).padding(top: 2),
|
content: post.content!,
|
||||||
)
|
attachments: post.attachments,
|
||||||
else
|
).padding(top: 2),
|
||||||
Expanded(
|
)
|
||||||
child: Text(
|
else
|
||||||
'postHasAttachments',
|
Expanded(
|
||||||
).plural(post.attachments.length),
|
child:
|
||||||
),
|
Text(
|
||||||
],
|
'postHasAttachments',
|
||||||
|
style: TextStyle(height: 2),
|
||||||
|
)
|
||||||
|
.plural(post.attachments.length)
|
||||||
|
.padding(top: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onOpen?.call();
|
onOpen?.call();
|
||||||
@@ -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),
|
||||||
@@ -193,81 +210,88 @@ class PostReplyPreview extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: (featuredReply!).map(
|
: (featuredReply!).map(
|
||||||
data:
|
data: (data) => ConstrainedBox(
|
||||||
(data) => Row(
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
spacing: 8,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
spacing: 8,
|
||||||
ProfilePictureWidget(
|
children: [
|
||||||
file: data.value?.publisher.picture,
|
ProfilePictureWidget(
|
||||||
radius: 12,
|
file: data.value?.publisher.picture,
|
||||||
).padding(top: 4),
|
radius: 12,
|
||||||
if (data.value?.content?.isNotEmpty ?? false)
|
).padding(top: 4),
|
||||||
Expanded(
|
if (data.value?.content?.isNotEmpty ?? false)
|
||||||
child: MarkdownTextContent(
|
Expanded(
|
||||||
content: data.value!.content!,
|
child: MarkdownTextContent(
|
||||||
attachments: data.value!.attachments,
|
content: data.value!.content!,
|
||||||
),
|
attachments: data.value!.attachments,
|
||||||
)
|
|
||||||
else
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'postHasAttachments',
|
|
||||||
).plural(data.value?.attachments.length ?? 0),
|
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
else
|
||||||
error:
|
Expanded(
|
||||||
(e) => Row(
|
child: Text(
|
||||||
spacing: 8,
|
'postHasAttachments',
|
||||||
children: [
|
).plural(data.value?.attachments.length ?? 0),
|
||||||
const Icon(Symbols.close, size: 18),
|
|
||||||
Text(e.error.toString()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
loading:
|
|
||||||
(_) => Row(
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
),
|
||||||
Text('loading').tr(),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final contentWidget =
|
|
||||||
isCompact
|
|
||||||
? itemWidget
|
|
||||||
: Container(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
error: (e) => Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
spacing: 8,
|
||||||
spacing: 4,
|
|
||||||
children: [
|
children: [
|
||||||
Text('repliesCount')
|
const Icon(Symbols.close, size: 18),
|
||||||
.plural(parent.repliesCount)
|
Text(e.error.toString()),
|
||||||
.fontSize(15)
|
],
|
||||||
.bold()
|
),
|
||||||
.padding(horizontal: 5),
|
loading: (_) => Row(
|
||||||
itemWidget,
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
Text('loading').tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return InkWell(
|
final contentWidget = isCompact
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
? itemBuilder(itemMaxWidth ?? MediaQuery.of(context).size.width)
|
||||||
|
: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Text('repliesCount')
|
||||||
|
.plural(parent.repliesCount)
|
||||||
|
.fontSize(15)
|
||||||
|
.bold()
|
||||||
|
.padding(horizontal: 5),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: itemBuilder(constraints.maxWidth),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
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,10 +515,9 @@ 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,
|
||||||
).padding(bottom: 4),
|
).padding(bottom: 4),
|
||||||
if (referencePost.isTruncated)
|
if (referencePost.isTruncated)
|
||||||
@@ -537,11 +561,10 @@ 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,15 +600,14 @@ class PostHeader extends StatelessWidget {
|
|||||||
spacing: 12,
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap:
|
onTap: isInteractive
|
||||||
isInteractive
|
? () {
|
||||||
? () {
|
context.pushNamed(
|
||||||
context.pushNamed(
|
'publisherProfile',
|
||||||
'publisherProfile',
|
pathParameters: {'name': item.publisher.name},
|
||||||
pathParameters: {'name': item.publisher.name},
|
);
|
||||||
);
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
|
||||||
child: ProfilePictureWidget(
|
child: ProfilePictureWidget(
|
||||||
file:
|
file:
|
||||||
item.publisher.picture ??
|
item.publisher.picture ??
|
||||||
@@ -606,19 +628,19 @@ class PostHeader extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child:
|
child:
|
||||||
(item.publisher.account != null &&
|
(item.publisher.account != null &&
|
||||||
item.publisher.type == 0)
|
item.publisher.type == 0)
|
||||||
? AccountName(
|
? AccountName(
|
||||||
hideOverlay: hideOverlay,
|
hideOverlay: hideOverlay,
|
||||||
account: item.publisher.account!,
|
account: item.publisher.account!,
|
||||||
textOverride: item.publisher.nick,
|
textOverride: item.publisher.nick,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
hideVerificationMark: true,
|
hideVerificationMark: true,
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
item.publisher.nick,
|
item.publisher.nick,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).bold(),
|
).bold(),
|
||||||
),
|
),
|
||||||
if (item.publisher.verification != null)
|
if (item.publisher.verification != null)
|
||||||
VerificationMark(
|
VerificationMark(
|
||||||
@@ -627,14 +649,13 @@ 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}',
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
).fontSize(11),
|
||||||
).fontSize(11),
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
...([
|
...([
|
||||||
@@ -673,8 +694,8 @@ class PostHeader extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
!isFullPost && isRelativeTime
|
!isFullPost && isRelativeTime
|
||||||
? (item.publishedAt ?? item.createdAt)!.formatRelative(
|
? (item.publishedAt ?? item.createdAt)!.formatRelative(
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
|
: (item.publishedAt ?? item.createdAt)!.formatSystem(),
|
||||||
).fontSize(10),
|
).fontSize(10),
|
||||||
],
|
],
|
||||||
@@ -734,15 +755,14 @@ 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',
|
pathParameters: {'slug': tag.slug},
|
||||||
pathParameters: {'slug': tag.slug},
|
);
|
||||||
);
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
|
||||||
child: Text('#${tag.name ?? tag.slug}'),
|
child: Text('#${tag.name ?? tag.slug}'),
|
||||||
),
|
),
|
||||||
if (!isFullPost && item.tags.length > 3)
|
if (!isFullPost && item.tags.length > 3)
|
||||||
@@ -761,15 +781,14 @@ 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',
|
pathParameters: {'slug': category.slug},
|
||||||
pathParameters: {'slug': category.slug},
|
);
|
||||||
);
|
}
|
||||||
}
|
: null,
|
||||||
: null,
|
|
||||||
child: Text(category.categoryDisplayTitle),
|
child: Text(category.categoryDisplayTitle),
|
||||||
),
|
),
|
||||||
if (!isFullPost && item.categories.length > 2)
|
if (!isFullPost && item.categories.length > 2)
|
||||||
@@ -798,12 +817,11 @@ 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,10 +954,9 @@ 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,
|
||||||
attachments: item.attachments,
|
attachments: item.attachments,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,29 +50,32 @@ class PostShuffleScreen extends HookConsumerWidget {
|
|||||||
controller: cardSwiperController,
|
controller: cardSwiperController,
|
||||||
cardsCount: items.length,
|
cardsCount: items.length,
|
||||||
isLoop: false,
|
isLoop: false,
|
||||||
cardBuilder: (
|
cardBuilder:
|
||||||
context,
|
(
|
||||||
index,
|
context,
|
||||||
horizontalOffsetPercentage,
|
index,
|
||||||
verticalOffsetPercentage,
|
horizontalOffsetPercentage,
|
||||||
) {
|
verticalOffsetPercentage,
|
||||||
return Center(
|
) {
|
||||||
child: ConstrainedBox(
|
return Center(
|
||||||
constraints: BoxConstraints(maxWidth: 540),
|
child: ConstrainedBox(
|
||||||
child: SingleChildScrollView(
|
constraints: BoxConstraints(maxWidth: 540),
|
||||||
child: Card(
|
child: SingleChildScrollView(
|
||||||
margin: EdgeInsets.zero,
|
child: Card(
|
||||||
child: ClipRRect(
|
margin: EdgeInsets.zero,
|
||||||
borderRadius: const BorderRadius.all(
|
child: ClipRRect(
|
||||||
Radius.circular(8),
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
child: PostActionableItem(
|
||||||
|
item: items[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: PostActionableItem(item: items[index]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
onEnd: () async {
|
onEnd: () async {
|
||||||
if (!postListNotifier.fetchedAll) {
|
if (!postListNotifier.fetchedAll) {
|
||||||
postListNotifier.fetchFurther();
|
postListNotifier.fetchFurther();
|
||||||
@@ -91,24 +98,23 @@ 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(
|
onPressed: () {
|
||||||
onPressed: () {
|
cardSwiperController.undo();
|
||||||
cardSwiperController.undo();
|
},
|
||||||
},
|
icon: const Icon(Symbols.arrow_left_alt),
|
||||||
icon: const Icon(Symbols.arrow_left_alt),
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
cardSwiperController.swipe(CardSwiperDirection.right);
|
||||||
cardSwiperController.swipe(CardSwiperDirection.right);
|
},
|
||||||
},
|
icon: const Icon(Symbols.arrow_right_alt),
|
||||||
icon: const Icon(Symbols.arrow_right_alt),
|
),
|
||||||
),
|
],
|
||||||
],
|
).padding(all: 8).center(),
|
||||||
).padding(all: 8).center(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
348
lib/widgets/posts/post_filter.dart
Normal file
348
lib/widgets/posts/post_filter.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: [
|
constraints: const BoxConstraints(maxWidth: 540),
|
||||||
Padding(
|
child: RealmListTile(realm: realm),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
).center();
|
||||||
child:
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 540),
|
|
||||||
child: RealmCard(realm: realm),
|
|
||||||
).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),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:island/pods/site_files.dart';
|
|||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/sites/file_upload_dialog.dart';
|
import 'package:island/widgets/sites/file_upload_dialog.dart';
|
||||||
import 'package:island/widgets/sites/file_item.dart';
|
import 'package:island/widgets/sites/file_item.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
@@ -53,6 +55,9 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
icon: const Icon(Symbols.upload),
|
icon: const Icon(Symbols.upload),
|
||||||
onSelected: (String choice) async {
|
onSelected: (String choice) async {
|
||||||
|
if (!kIsWeb) {
|
||||||
|
await Permission.storage.request();
|
||||||
|
}
|
||||||
List<File> files = [];
|
List<File> files = [];
|
||||||
List<Map<String, dynamic>>? results;
|
List<Map<String, dynamic>>? results;
|
||||||
if (choice == 'files') {
|
if (choice == 'files') {
|
||||||
@@ -65,17 +70,17 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
selectedFiles.files.isEmpty) {
|
selectedFiles.files.isEmpty) {
|
||||||
return; // User canceled
|
return; // User canceled
|
||||||
}
|
}
|
||||||
files =
|
files = selectedFiles.files
|
||||||
selectedFiles.files
|
.map((f) => File(f.path!))
|
||||||
.map((f) => File(f.path!))
|
.toList();
|
||||||
.toList();
|
|
||||||
} else if (choice == 'folder') {
|
} else if (choice == 'folder') {
|
||||||
final dirPath =
|
final dirPath = await FilePicker.platform
|
||||||
await FilePicker.platform.getDirectoryPath();
|
.getDirectoryPath();
|
||||||
if (dirPath == null) return;
|
if (dirPath == null) return;
|
||||||
results = await _getFilesRecursive(dirPath);
|
results = await _getFilesRecursive(dirPath);
|
||||||
files =
|
files = results
|
||||||
results.map((m) => m['file'] as File).toList();
|
.map((m) => m['file'] as File)
|
||||||
|
.toList();
|
||||||
if (files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
showSnackBar('noFilesFoundInFolder'.tr());
|
showSnackBar('noFilesFoundInFolder'.tr());
|
||||||
return;
|
return;
|
||||||
@@ -88,51 +93,46 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) => FileUploadDialog(
|
||||||
(context) => FileUploadDialog(
|
selectedFiles: files,
|
||||||
selectedFiles: files,
|
site: site,
|
||||||
site: site,
|
relativePaths: results
|
||||||
relativePaths:
|
?.map((m) => m['relativePath'] as String)
|
||||||
results
|
.toList(),
|
||||||
?.map(
|
onUploadComplete: () {
|
||||||
(m) => m['relativePath'] as String,
|
// Refresh file list
|
||||||
)
|
ref.invalidate(
|
||||||
.toList(),
|
siteFilesProvider(
|
||||||
onUploadComplete: () {
|
siteId: site.id,
|
||||||
// Refresh file list
|
path: currentPath.value,
|
||||||
ref.invalidate(
|
),
|
||||||
siteFilesProvider(
|
);
|
||||||
siteId: site.id,
|
},
|
||||||
path: currentPath.value,
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemBuilder:
|
itemBuilder: (BuildContext context) => [
|
||||||
(BuildContext context) => [
|
PopupMenuItem<String>(
|
||||||
PopupMenuItem<String>(
|
value: 'files',
|
||||||
value: 'files',
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Icon(Symbols.file_copy),
|
||||||
Icon(Symbols.file_copy),
|
Gap(12),
|
||||||
Gap(12),
|
Text('siteFiles'.tr()),
|
||||||
Text('siteFiles'.tr()),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
PopupMenuItem<String>(
|
||||||
PopupMenuItem<String>(
|
value: 'folder',
|
||||||
value: 'folder',
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Icon(Symbols.folder),
|
||||||
Icon(Symbols.folder),
|
Gap(12),
|
||||||
Gap(12),
|
Text('siteFolder'.tr()),
|
||||||
Text('siteFolder'.tr()),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
horizontal: -4,
|
horizontal: -4,
|
||||||
@@ -156,19 +156,17 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Symbols.arrow_back),
|
icon: Icon(Symbols.arrow_back),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final pathParts =
|
final pathParts = currentPath.value!
|
||||||
currentPath.value!
|
.split('/')
|
||||||
.split('/')
|
.where((part) => part.isNotEmpty)
|
||||||
.where((part) => part.isNotEmpty)
|
.toList();
|
||||||
.toList();
|
|
||||||
if (pathParts.isEmpty) {
|
if (pathParts.isEmpty) {
|
||||||
currentPath.value = null;
|
currentPath.value = null;
|
||||||
} else {
|
} else {
|
||||||
pathParts.removeLast();
|
pathParts.removeLast();
|
||||||
currentPath.value =
|
currentPath.value = pathParts.isEmpty
|
||||||
pathParts.isEmpty
|
? null
|
||||||
? null
|
: pathParts.join('/');
|
||||||
: pathParts.join('/');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visualDensity: const VisualDensity(
|
visualDensity: const VisualDensity(
|
||||||
@@ -185,11 +183,10 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
child: Text('siteRoot'.tr()),
|
child: Text('siteRoot'.tr()),
|
||||||
),
|
),
|
||||||
...() {
|
...() {
|
||||||
final parts =
|
final parts = currentPath.value!
|
||||||
currentPath.value!
|
.split('/')
|
||||||
.split('/')
|
.where((part) => part.isNotEmpty)
|
||||||
.where((part) => part.isNotEmpty)
|
.toList();
|
||||||
.toList();
|
|
||||||
final widgets = <Widget>[];
|
final widgets = <Widget>[];
|
||||||
String currentBuilder = '';
|
String currentBuilder = '';
|
||||||
for (final part in parts) {
|
for (final part in parts) {
|
||||||
@@ -200,8 +197,8 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
widgets.addAll([
|
widgets.addAll([
|
||||||
const Text(' / '),
|
const Text(' / '),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap:
|
onTap: () =>
|
||||||
() => currentPath.value = pathToSet,
|
currentPath.value = pathToSet,
|
||||||
child: Text(part),
|
child: Text(part),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -253,33 +250,31 @@ class FileManagementSection extends HookConsumerWidget {
|
|||||||
return FileItem(
|
return FileItem(
|
||||||
file: file,
|
file: file,
|
||||||
site: site,
|
site: site,
|
||||||
onNavigateDirectory:
|
onNavigateDirectory: (path) =>
|
||||||
(path) => currentPath.value = path,
|
currentPath.value = path,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading:
|
loading: () =>
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
const Center(child: CircularProgressIndicator()),
|
||||||
error:
|
error: (error, stack) => Center(
|
||||||
(error, stack) => Center(
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
Text('failedToLoadFiles'.tr()),
|
||||||
Text('failedToLoadFiles'.tr()),
|
const Gap(8),
|
||||||
const Gap(8),
|
ElevatedButton(
|
||||||
ElevatedButton(
|
onPressed: () => ref.invalidate(
|
||||||
onPressed:
|
siteFilesProvider(
|
||||||
() => ref.invalidate(
|
siteId: site.id,
|
||||||
siteFilesProvider(
|
path: currentPath.value,
|
||||||
siteId: site.id,
|
|
||||||
path: currentPath.value,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text('retry'.tr()),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
child: Text('retry'.tr()),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,64 +19,50 @@ class SiteActionMenu extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return PopupMenuButton<String>(
|
return PopupMenuButton<String>(
|
||||||
itemBuilder:
|
itemBuilder: (context) => [
|
||||||
(context) => [
|
PopupMenuItem(
|
||||||
PopupMenuItem(
|
value: 'edit',
|
||||||
value: 'edit',
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Icon(
|
||||||
Icon(
|
Symbols.edit,
|
||||||
Symbols.edit,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
Text('edit'.tr()),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const Gap(16),
|
||||||
const PopupMenuDivider(),
|
Text('edit'.tr()),
|
||||||
PopupMenuItem(
|
],
|
||||||
value: 'delete',
|
),
|
||||||
child: Row(
|
),
|
||||||
children: [
|
const PopupMenuDivider(),
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
PopupMenuItem(
|
||||||
const Gap(16),
|
value: 'delete',
|
||||||
Text('delete'.tr()).textColor(Colors.red),
|
child: Row(
|
||||||
],
|
children: [
|
||||||
),
|
const Icon(Symbols.delete, color: Colors.red),
|
||||||
),
|
const Gap(16),
|
||||||
],
|
Text('delete'.tr()).textColor(Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
onSelected: (value) async {
|
onSelected: (value) async {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'edit':
|
case 'edit':
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder:
|
builder: (context) =>
|
||||||
(context) => SiteForm(pubName: pubName, siteSlug: site.slug),
|
SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
// Refresh site data after potential edit
|
// Refresh site data after potential edit
|
||||||
ref.invalidate(publicationSiteDetailProvider(pubName, site.slug));
|
ref.invalidate(publicationSiteDetailProvider(pubName, site.slug));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
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('publicationSiteDeleteConfirm'.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) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/drive_task.dart';
|
import 'package:island/models/drive_task.dart';
|
||||||
import 'package:island/pods/upload_tasks.dart';
|
import 'package:island/pods/drive/upload_tasks.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@@ -223,14 +223,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
if (!isExpanded && activeTasks.isNotEmpty)
|
if (!isExpanded && activeTasks.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
_getOverallProgressText(activeTasks),
|
_getOverallProgressText(activeTasks),
|
||||||
style: Theme.of(
|
style: Theme.of(context)
|
||||||
context,
|
.textTheme
|
||||||
).textTheme.bodySmall?.copyWith(
|
.bodySmall
|
||||||
color:
|
?.copyWith(
|
||||||
Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant,
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -244,10 +244,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: _getOverallProgress(activeTasks),
|
value: _getOverallProgress(activeTasks),
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
backgroundColor:
|
backgroundColor: Theme.of(
|
||||||
Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.surfaceContainerHighest,
|
||||||
).colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -263,8 +262,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed: () =>
|
||||||
() => onExpansionChanged?.call(!isExpanded),
|
onExpansionChanged?.call(!isExpanded),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
),
|
),
|
||||||
@@ -297,20 +296,18 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.clear_all,
|
Symbols.clear_all,
|
||||||
size: 18,
|
size: 18,
|
||||||
color:
|
color: Theme.of(
|
||||||
Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.onSurfaceVariant,
|
||||||
).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
taskNotifier.clearCompletedTasks();
|
taskNotifier.clearCompletedTasks();
|
||||||
onExpansionChanged?.call(false);
|
onExpansionChanged?.call(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
tileColor:
|
tileColor: Theme.of(
|
||||||
Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.surfaceContainerHighest,
|
||||||
).colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -326,17 +323,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
leading: Icon(
|
leading: Icon(
|
||||||
Symbols.clear_all,
|
Symbols.clear_all,
|
||||||
size: 18,
|
size: 18,
|
||||||
color:
|
color: Theme.of(
|
||||||
Theme.of(context).colorScheme.error,
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
taskNotifier.clearAllTasks();
|
taskNotifier.clearAllTasks();
|
||||||
onExpansionChanged?.call(false);
|
onExpansionChanged?.call(false);
|
||||||
},
|
},
|
||||||
tileColor:
|
tileColor: Theme.of(
|
||||||
Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.surfaceContainerHighest,
|
||||||
).colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -556,8 +553,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: widget.task.progress,
|
value: widget.task.progress,
|
||||||
strokeWidth: 2.5,
|
strokeWidth: 2.5,
|
||||||
backgroundColor:
|
backgroundColor: Theme.of(
|
||||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
context,
|
||||||
|
).colorScheme.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -604,10 +602,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
color = Theme.of(context).colorScheme.secondary;
|
color = Theme.of(context).colorScheme.secondary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.inProgress:
|
case DriveTaskStatus.inProgress:
|
||||||
icon =
|
icon = widget.task.type == 'FileDownload'
|
||||||
widget.task.type == 'FileDownload'
|
? Symbols.download
|
||||||
? Symbols.download
|
: Symbols.upload;
|
||||||
: Symbols.upload;
|
|
||||||
color = Theme.of(context).colorScheme.primary;
|
color = Theme.of(context).colorScheme.primary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.paused:
|
case DriveTaskStatus.paused:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:island/models/webfeed.dart';
|
import 'package:island/models/webfeed.dart';
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
class WebArticleCard extends StatelessWidget {
|
class WebArticleCard extends StatelessWidget {
|
||||||
final SnWebArticle article;
|
final SnWebArticle article;
|
||||||
@@ -22,9 +23,6 @@ class WebArticleCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
|
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -32,108 +30,41 @@ class WebArticleCard extends StatelessWidget {
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _onTap(context),
|
onTap: () => _onTap(context),
|
||||||
child: AspectRatio(
|
child: Column(
|
||||||
aspectRatio: 16 / 9,
|
children: [
|
||||||
child: Stack(
|
if (article.preview?.imageUrl != null)
|
||||||
fit: StackFit.expand,
|
AspectRatio(
|
||||||
children: [
|
aspectRatio: 16 / 9,
|
||||||
// Image or fallback
|
child: CachedNetworkImage(
|
||||||
article.preview?.imageUrl != null
|
imageUrl: article.preview!.imageUrl!,
|
||||||
? CachedNetworkImage(
|
fit: BoxFit.cover,
|
||||||
imageUrl: article.preview!.imageUrl!,
|
width: double.infinity,
|
||||||
fit: BoxFit.cover,
|
height: double.infinity,
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
)
|
|
||||||
: ColoredBox(
|
|
||||||
color: colorScheme.secondaryContainer,
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.article_outlined,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Gradient overlay
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.black.withOpacity(0.7),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Title
|
ListTile(
|
||||||
Align(
|
isThreeLine: true,
|
||||||
alignment: Alignment.bottomLeft,
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
child: Container(
|
horizontal: 20,
|
||||||
padding: const EdgeInsets.only(
|
vertical: 4,
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (showDetails)
|
|
||||||
const SizedBox(height: 8)
|
|
||||||
else
|
|
||||||
Spacer(),
|
|
||||||
Text(
|
|
||||||
article.title,
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
height: 1.3,
|
|
||||||
),
|
|
||||||
maxLines: showDetails ? 3 : 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (showDetails &&
|
|
||||||
article.author?.isNotEmpty == true) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
article.author!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.white.withOpacity(0.9),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (showDetails) const Spacer(),
|
|
||||||
if (showDetails && article.publishedAt != null) ...[
|
|
||||||
Text(
|
|
||||||
'${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 9,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
],
|
|
||||||
Text(
|
|
||||||
article.feed?.title ?? 'Unknown Source',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 9,
|
|
||||||
color: Colors.white70,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
),
|
title: Text(article.title),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${article.createdAt.formatSystem()} · ${article.createdAt.formatRelative(context)}',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
article.feed?.title ?? 'Unknown Source',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user